diff --git a/libs/qec/include/cudaq/qec/plugin_loader.h b/libs/qec/include/cudaq/qec/plugin_loader.h
new file mode 100644
index 0000000..280b1f1
--- /dev/null
+++ b/libs/qec/include/cudaq/qec/plugin_loader.h
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Copyright (c) 2024 NVIDIA Corporation & Affiliates.                         *
+ * All rights reserved.                                                        *
+ *                                                                             *
+ * This source code and the accompanying materials are made available under    *
+ * the terms of the Apache License 2.0 which accompanies this distribution.    *
+ ******************************************************************************/
+
+#ifndef PLUGIN_LOADER_H
+#define PLUGIN_LOADER_H
+
+#include <dlfcn.h>
+#include <map>
+#include <memory>
+#include <string>
+
+/// @brief Enum to define different types of plugins
+enum class PluginType {
+  DECODER, // Decoder plugins
+  CODE     // QEC codes plugins
+           // Add other plugin types here as needed
+};
+
+/// @brief A struct to store plugin handle with its type
+struct PluginHandle {
+  std::shared_ptr<void> handle; // Pointer to the shared library handle. This is
+                                // the result of dlopen() function.
+  PluginType type;              // Type of the plugin (e.g., decoder, code, etc)
+};
+
+/// @brief Function to load plugins from a directory based on type
+/// @param plugin_dir The directory where the plugins are located
+/// @param type The type of plugins to load. Only plugins of this type will be
+/// loaded.
+void load_plugins(const std::string &plugin_dir, PluginType type);
+
+/// @brief Function to clean up loaded plugins of a specific type
+/// @param type The type of plugins to clean up. Only plugins of this type will
+/// be cleaned up.
+void cleanup_plugins(PluginType type);
+
+#endif // PLUGIN_LOADER_H
diff --git a/libs/qec/lib/CMakeLists.txt b/libs/qec/lib/CMakeLists.txt
index 420efe2..55b5433 100644
--- a/libs/qec/lib/CMakeLists.txt
+++ b/libs/qec/lib/CMakeLists.txt
@@ -9,6 +9,7 @@
 set(LIBRARY_NAME cudaq-qec)
 
 add_compile_options(-Wno-attributes) 
+add_compile_definitions(DECODER_PLUGIN_DIR="${CMAKE_INSTALL_PREFIX}/lib/decoder-plugins")
 
 # FIXME?: This must be a shared library. Trying to build a static one will fail.
 add_library(${LIBRARY_NAME} SHARED
@@ -17,8 +18,10 @@ add_library(${LIBRARY_NAME} SHARED
   decoder.cpp
   experiments.cpp
   decoders/single_error_lut.cpp
+  plugin_loader.cpp
 )
 
+add_subdirectory(decoders/plugins/example)
 add_subdirectory(codes)
 add_subdirectory(device) 
 
diff --git a/libs/qec/lib/decoder.cpp b/libs/qec/lib/decoder.cpp
index 9da1a07..9f4cc56 100644
--- a/libs/qec/lib/decoder.cpp
+++ b/libs/qec/lib/decoder.cpp
@@ -7,7 +7,10 @@
  ******************************************************************************/
 
 #include "cudaq/qec/decoder.h"
+#include "cudaq/qec/plugin_loader.h"
 #include <cassert>
+#include <dlfcn.h>
+#include <filesystem>
 #include <vector>
 
 INSTANTIATE_REGISTRY(cudaq::qec::decoder, const cudaqx::tensor<uint8_t> &)
@@ -71,3 +74,15 @@ std::unique_ptr<decoder> get_decoder(const std::string &name,
   return decoder::get(name, H, options);
 }
 } // namespace cudaq::qec
+
+// Constructor function for auto-loading plugins
+__attribute__((constructor)) void load_decoder_plugins() {
+  // Load plugins from the decoder-specific plugin directory
+  load_plugins(DECODER_PLUGIN_DIR, PluginType::DECODER);
+}
+
+// Destructor function to clean up only decoder plugins
+__attribute__((destructor)) void cleanup_decoder_plugins() {
+  // Clean up decoder-specific plugins
+  cleanup_plugins(PluginType::DECODER);
+}
\ No newline at end of file
diff --git a/libs/qec/lib/decoders/plugins/example/CMakeLists.txt b/libs/qec/lib/decoders/plugins/example/CMakeLists.txt
new file mode 100644
index 0000000..2cb1ea3
--- /dev/null
+++ b/libs/qec/lib/decoders/plugins/example/CMakeLists.txt
@@ -0,0 +1,72 @@
+# ============================================================================ #
+# Copyright (c) 2024 NVIDIA Corporation & Affiliates.                          #
+# All rights reserved.                                                         #
+#                                                                              #
+# This source code and the accompanying materials are made available under     #
+# the terms of the Apache License 2.0 which accompanies this distribution.     #
+# ============================================================================ #
+
+cmake_minimum_required(VERSION 3.28 FATAL_ERROR)
+
+set(MODULE_NAME "cudaq-qec-example")
+
+project(${MODULE_NAME})
+
+# Specify the source file for the plugin
+set(PLUGIN_SRC
+  single_error_lut_example.cpp
+  # single_error_lut_example2.cpp // add other decoder source files here
+)
+
+# Create the shared library
+add_library(${MODULE_NAME} SHARED ${PLUGIN_SRC})
+
+# Set the include directories for dependencies
+target_include_directories(${MODULE_NAME}
+  PUBLIC
+    ${CMAKE_SOURCE_DIR}/libs/qec/include
+    ${CMAKE_SOURCE_DIR}/libs/core/include
+)
+
+# Link with required libraries 
+target_link_libraries(${MODULE_NAME}
+  PUBLIC
+    cudaqx-core
+    cudaq::cudaq
+    cudaq::cudaq-spin
+  PRIVATE
+    cudaq::cudaq-common
+    cudaq-qec
+)
+
+set_target_properties(${MODULE_NAME} PROPERTIES
+  LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib/decoder-plugins
+)
+
+# RPATH configuration 
+# ==============================================================================
+
+if (NOT SKBUILD)
+  set_target_properties(${LIBRARY_NAME} PROPERTIES
+    BUILD_RPATH "$ORIGIN"
+    INSTALL_RPATH "$ORIGIN:$ORIGIN/.."
+  )
+
+  # Let CMake automatically add paths of linked libraries to the RPATH:
+  set_target_properties(${LIBRARY_NAME} PROPERTIES
+    INSTALL_RPATH_USE_LINK_PATH TRUE)
+else()
+  # CUDA-Q install its libraries in site-packages/lib (or dist-packages/lib)
+  # Thus, we need the $ORIGIN/../lib
+  set_target_properties(${LIBRARY_NAME} PROPERTIES
+    INSTALL_RPATH "$ORIGIN/../../lib"
+  )
+endif()
+
+# Install
+# ==============================================================================
+
+install(TARGETS ${MODULE_NAME}
+  COMPONENT qec-lib-plugins
+  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/decoder-plugins
+)
diff --git a/libs/qec/lib/decoders/plugins/example/decoder_plugins_demo.cpp b/libs/qec/lib/decoders/plugins/example/decoder_plugins_demo.cpp
new file mode 100644
index 0000000..f6149ac
--- /dev/null
+++ b/libs/qec/lib/decoders/plugins/example/decoder_plugins_demo.cpp
@@ -0,0 +1,112 @@
+/*******************************************************************************
+ * Copyright (c) 2024 NVIDIA Corporation & Affiliates.                         *
+ * All rights reserved.                                                        *
+ *                                                                             *
+ * This source code and the accompanying materials are made available under    *
+ * the terms of the Apache License 2.0 which accompanies this distribution.    *
+ ******************************************************************************/
+
+// This example shows how to use decoders from decoder plugins
+//
+// Compile and run with
+// nvq++ --enable-mlir -lcudaq-qec decoder_plugins_demo.cpp -o
+// decoder_plugins_demo
+// ./decoder_plugins_demo
+
+#include <dlfcn.h>
+#include <filesystem>
+#include <iostream>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "cudaq.h"
+#include "cudaq/qec/decoder.h"
+#include "cudaq/qec/experiments.h"
+
+int main() {
+  auto steane = cudaq::qec::get_code("steane");
+  auto Hz = steane->get_parity_z();
+  std::vector<size_t> t_shape = Hz.shape();
+
+  std::cout << "Hz.shape():\n";
+  for (size_t elem : t_shape)
+    std::cout << elem << " ";
+  std::cout << "\n";
+
+  std::cout << "Hz:\n";
+  Hz.dump();
+
+  auto Lz = steane->get_observables_x();
+  std::cout << "Lz:\n";
+  Lz.dump();
+
+  double p = 0.2;
+  size_t nShots = 5;
+
+  // Check for available decoders
+  for (auto &name : cudaq::qec::decoder::get_registered())
+    printf("Decoder: %s\n", name.c_str());
+  // create a decoder from the plugins
+  auto lut_decoder = cudaq::qec::get_decoder("single_error_lut_example", Hz);
+
+  std::cout << "nShots: " << nShots << "\n";
+
+  // May want a order-2 tensor of syndromes
+  // access tensor by stride to write in an entire syndrome
+  cudaqx::tensor<uint8_t> syndrome({Hz.shape()[0]});
+
+  int nErrors = 0;
+  for (size_t shot = 0; shot < nShots; ++shot) {
+    std::cout << "shot: " << shot << "\n";
+    auto shot_data = cudaq::qec::generate_random_bit_flips(Hz.shape()[1], p);
+    std::cout << "shot data\n";
+    shot_data.dump();
+
+    auto observable_z_data = Lz.dot(shot_data);
+    observable_z_data = observable_z_data % 2;
+    std::cout << "Data Lz state:\n";
+    observable_z_data.dump();
+
+    auto syndrome = Hz.dot(shot_data);
+    syndrome = syndrome % 2;
+    std::cout << "syndrome:\n";
+    syndrome.dump();
+
+    auto [converged, v_result] = lut_decoder->decode(syndrome);
+    cudaqx::tensor<uint8_t> result_tensor;
+    // v_result is a std::vector<float_t>, of soft information. We'll convert
+    // this to hard information and store as a tensor<uint8_t>.
+    cudaq::qec::convert_vec_soft_to_tensor_hard(v_result, result_tensor);
+    std::cout << "decode result:\n";
+    result_tensor.dump();
+
+    // check observable result
+    auto decoded_observable_z = Lz.dot(result_tensor);
+    std::cout << "decoded observable:\n";
+    decoded_observable_z.dump();
+
+    // check how many observable operators were decoded correctly
+    // observable_z_data == decoded_observable_z This maps onto element wise
+    // addition (mod 2)
+    auto observable_flips = decoded_observable_z + observable_z_data;
+    observable_flips = observable_flips % 2;
+    std::cout << "Logical errors:\n";
+    observable_flips.dump();
+    std::cout << "\n";
+
+    // shot counts as a observable error unless all observables are correct
+    if (observable_flips.any()) {
+      nErrors++;
+    }
+  }
+  std::cout << "Total logical errors: " << nErrors << "\n";
+
+  // Full data gen in function call
+  auto [syn, data] = cudaq::qec::sample_code_capacity(Hz, nShots, p);
+  std::cout << "Numerical experiment:\n";
+  std::cout << "Data:\n";
+  data.dump();
+  std::cout << "Syn:\n";
+  syn.dump();
+}
diff --git a/libs/qec/lib/decoders/plugins/example/single_error_lut_example.cpp b/libs/qec/lib/decoders/plugins/example/single_error_lut_example.cpp
new file mode 100644
index 0000000..1df26ef
--- /dev/null
+++ b/libs/qec/lib/decoders/plugins/example/single_error_lut_example.cpp
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates.                  *
+ * All rights reserved.                                                        *
+ *                                                                             *
+ * This source code and the accompanying materials are made available under    *
+ * the terms of the Apache License 2.0 which accompanies this distribution.    *
+ ******************************************************************************/
+
+#include "cudaq/qec/decoder.h"
+#include <cassert>
+#include <map>
+#include <vector>
+
+namespace cudaq::qec {
+
+/// @brief This is a simple LUT (LookUp Table) decoder that demonstrates how to
+/// build a simple decoder that can correctly decode errors during a single bit
+/// flip in the block.
+class single_error_lut_example : public decoder {
+private:
+  std::map<std::string, std::size_t> single_qubit_err_signatures;
+
+public:
+  single_error_lut_example(const cudaqx::tensor<uint8_t> &H,
+                           const cudaqx::heterogeneous_map &params)
+      : decoder(H) {
+    // Decoder-specific constructor arguments can be placed in `params`.
+
+    // Build a lookup table for an error on each possible qubit
+
+    // For each qubit with a possible error, calculate an error signature.
+    for (std::size_t qErr = 0; qErr < block_size; qErr++) {
+      std::string err_sig(syndrome_size, '0');
+      for (std::size_t r = 0; r < syndrome_size; r++) {
+        bool syndrome = 0;
+        // Toggle syndrome on every "1" entry in the row.
+        // Except if there is an error on this qubit (c == qErr).
+        for (std::size_t c = 0; c < block_size; c++)
+          syndrome ^= (c != qErr) && H.at({r, c});
+        err_sig[r] = syndrome ? '1' : '0';
+      }
+      // printf("Adding err_sig=%s for qErr=%lu\n", err_sig.c_str(), qErr);
+      single_qubit_err_signatures.insert({err_sig, qErr});
+    }
+  }
+
+  virtual decoder_result decode(const std::vector<float_t> &syndrome) {
+    // This is a simple decoder that simply results
+    decoder_result result{false, std::vector<float_t>(block_size, 0.0)};
+
+    // Convert syndrome to a string
+    std::string syndrome_str(syndrome.size(), '0');
+    assert(syndrome_str.length() == syndrome_size);
+    bool anyErrors = false;
+    for (std::size_t i = 0; i < syndrome_size; i++) {
+      if (syndrome[i] >= 0.5) {
+        syndrome_str[i] = '1';
+        anyErrors = true;
+      }
+    }
+
+    if (!anyErrors) {
+      result.converged = true;
+      return result;
+    }
+
+    auto it = single_qubit_err_signatures.find(syndrome_str);
+    if (it != single_qubit_err_signatures.end()) {
+      assert(it->second < block_size);
+      result.converged = true;
+      result.result[it->second] = 1.0;
+    } else {
+      // Leave result.converged set to false.
+    }
+
+    return result;
+  }
+
+  virtual ~single_error_lut_example() {}
+
+  CUDAQ_EXTENSION_CUSTOM_CREATOR_FUNCTION(
+      single_error_lut_example, static std::unique_ptr<decoder> create(
+                                    const cudaqx::tensor<uint8_t> &H,
+                                    const cudaqx::heterogeneous_map &params) {
+        return std::make_unique<single_error_lut_example>(H, params);
+      })
+};
+
+CUDAQ_REGISTER_TYPE(single_error_lut_example)
+
+} // namespace cudaq::qec
diff --git a/libs/qec/lib/plugin_loader.cpp b/libs/qec/lib/plugin_loader.cpp
new file mode 100644
index 0000000..35a69f5
--- /dev/null
+++ b/libs/qec/lib/plugin_loader.cpp
@@ -0,0 +1,59 @@
+/****************************************************************-*- C++ -*-****
+ * Copyright (c) 2024 NVIDIA Corporation & Affiliates.                         *
+ * All rights reserved.                                                        *
+ *                                                                             *
+ * This source code and the accompanying materials are made available under    *
+ * the terms of the Apache License 2.0 which accompanies this distribution.    *
+ ******************************************************************************/
+
+#include "cudaq/qec/plugin_loader.h"
+#include <filesystem>
+#include <iostream>
+
+namespace fs = std::filesystem;
+
+static std::map<std::string, PluginHandle> &get_plugin_handles() {
+  static std::map<std::string, PluginHandle> plugin_handles;
+  return plugin_handles;
+}
+
+// Function to load plugins from a directory based on their type
+void load_plugins(const std::string &plugin_dir, PluginType type) {
+  if (!fs::exists(plugin_dir)) {
+    std::cerr << "WARNING: Plugin directory does not exist: " << plugin_dir
+              << std::endl;
+    return;
+  }
+  for (const auto &entry : fs::directory_iterator(plugin_dir)) {
+    if (entry.path().extension() == ".so") {
+      void *raw_handle = dlopen(entry.path().c_str(), RTLD_NOW);
+      if (raw_handle) {
+        // Custom deleter ensures dlclose is called
+        auto deleter = [](void *h) {
+          if (h)
+            dlclose(h);
+        };
+
+        get_plugin_handles().emplace(
+            entry.path().filename().string(),
+            PluginHandle{std::shared_ptr<void>(raw_handle, deleter), type});
+      } else {
+        std::cerr << "ERROR: Failed to load plugin: " << entry.path()
+                  << " Error: " << dlerror() << std::endl;
+      }
+    }
+  }
+}
+
+// Function to clean up the plugin handles
+void cleanup_plugins(PluginType type) {
+  auto &handles = get_plugin_handles();
+  auto it = handles.begin();
+  while (it != handles.end()) {
+    if (it->second.type == type) {
+      it = handles.erase(it); // dlclose is handled by the custom deleter
+    } else {
+      ++it;
+    }
+  }
+}
diff --git a/libs/qec/python/bindings/py_decoder.cpp b/libs/qec/python/bindings/py_decoder.cpp
index 8b6d591..11b6789 100644
--- a/libs/qec/python/bindings/py_decoder.cpp
+++ b/libs/qec/python/bindings/py_decoder.cpp
@@ -14,6 +14,7 @@
 #include "common/Logger.h"
 
 #include "cudaq/qec/decoder.h"
+#include "cudaq/qec/plugin_loader.h"
 
 #include "type_casters.h"
 #include "utils.h"
@@ -70,6 +71,13 @@ std::unordered_map<std::string, std::function<py::object(
     PyDecoderRegistry::registry;
 
 void bindDecoder(py::module &mod) {
+  // Required by all plugin classes
+  auto cleanup_callback = []() {
+    // Change the type to the correct plugin type
+    cleanup_plugins(PluginType::DECODER);
+  };
+  // This ensures the correct shutdown sequence
+  mod.add_object("_cleanup", py::capsule(cleanup_callback));
 
   auto qecmod = py::hasattr(mod, "qecrt")
                     ? mod.attr("qecrt").cast<py::module_>()
diff --git a/libs/qec/python/tests/test_decoder.py b/libs/qec/python/tests/test_decoder.py
index 59e1cde..e889042 100644
--- a/libs/qec/python/tests/test_decoder.py
+++ b/libs/qec/python/tests/test_decoder.py
@@ -40,6 +40,22 @@ def test_decoder_result_structure():
     assert len(result.result) == 10
 
 
+def test_decoder_plugin_initialization():
+    decoder = qec.get_decoder('single_error_lut_example', H)
+    assert decoder is not None
+    assert hasattr(decoder, 'decode')
+
+
+def test_decoder_plugin_result_structure():
+    decoder = qec.get_decoder('single_error_lut_example', H)
+    result = decoder.decode(create_test_syndrome())
+
+    assert hasattr(result, 'converged')
+    assert hasattr(result, 'result')
+    assert isinstance(result.converged, bool)
+    assert isinstance(result.result, list)
+
+
 def test_decoder_result_values():
     decoder = qec.get_decoder('example_byod', H)
     result = decoder.decode(create_test_syndrome())