diff --git a/src/libcamera/include/ipa_module.h b/src/libcamera/include/ipa_module.h
index a4c6dbd..bb03407 100644
--- a/src/libcamera/include/ipa_module.h
+++ b/src/libcamera/include/ipa_module.h
@@ -7,9 +7,10 @@
 #ifndef __LIBCAMERA_IPA_MODULE_H__
 #define __LIBCAMERA_IPA_MODULE_H__
 
-#include <string>
-
+#include <libcamera/ipa/ipa_interface.h>
 #include <libcamera/ipa/ipa_module_info.h>
+#include <memory>
+#include <string>
 
 namespace libcamera {
 
@@ -17,16 +18,26 @@ class IPAModule
 {
 public:
 	explicit IPAModule(const std::string &libPath);
+	~IPAModule();
 
 	bool isValid() const;
 
 	const struct IPAModuleInfo &info() const;
 
+	bool load();
+
+	std::unique_ptr<IPAInterface> createInstance();
+
 private:
 	struct IPAModuleInfo info_;
 
 	std::string libPath_;
 	bool valid_;
+	bool loaded_;
+
+	void *dlhandle_;
+	typedef IPAInterface *(*ipaiFactory)(void);
+	ipaiFactory ipaCreate_;
 
 	int loadIPAModuleInfo();
 };
diff --git a/src/libcamera/ipa_module.cpp b/src/libcamera/ipa_module.cpp
index 6e68934..9bb0594 100644
--- a/src/libcamera/ipa_module.cpp
+++ b/src/libcamera/ipa_module.cpp
@@ -7,6 +7,7 @@
 
 #include "ipa_module.h"
 
+#include <dlfcn.h>
 #include <elf.h>
 #include <errno.h>
 #include <fcntl.h>
@@ -242,13 +243,11 @@ int elfLoadSymbol(void *dst, size_t size, void *map, size_t soSize,
  * The IPA module shared object file must be of the same endianness and
  * bitness as libcamera.
  *
- * \todo load funtions from the IPA to be used by pipelines
- *
  * The caller shall call the isValid() method after constructing an
  * IPAModule instance to verify the validity of the IPAModule.
  */
 IPAModule::IPAModule(const std::string &libPath)
-	: libPath_(libPath), valid_(false)
+	: libPath_(libPath), valid_(false), loaded_(false)
 {
 	if (loadIPAModuleInfo() < 0)
 		return;
@@ -256,6 +255,12 @@ IPAModule::IPAModule(const std::string &libPath)
 	valid_ = true;
 }
 
+IPAModule::~IPAModule()
+{
+	if (dlhandle_)
+		dlclose(dlhandle_);
+}
+
 int IPAModule::loadIPAModuleInfo()
 {
 	int fd = open(libPath_.c_str(), O_RDONLY);
@@ -325,4 +330,73 @@ const struct IPAModuleInfo &IPAModule::info() const
 	return info_;
 }
 
+/**
+ * \brief Load the IPA implementation factory from the shared object
+ *
+ * The IPA module shared object implements an IPAInterface class to be used
+ * by pipeline handlers. This function loads the factory function from the
+ * shared object. Later, createInstance() can be called to instantiate the
+ * IPAInterface.
+ *
+ * This function only needs to be called successfully once, after which
+ * createInstance can be called as many times as IPAInterface instances are
+ * needed.
+ *
+ * Calling this function on an invalid module (as returned by isValid()) is
+ * an error.
+ *
+ * \return true if load was successful, or already loaded, and false otherwise
+ */
+bool IPAModule::load()
+{
+	if (!valid_)
+		return false;
+
+	if (loaded_)
+		return true;
+
+	dlhandle_ = dlopen(libPath_.c_str(), RTLD_LAZY);
+	if (!dlhandle_) {
+		LOG(IPAModule, Error) << "Failed to open IPA module shared object"
+				      << dlerror();
+		return false;
+	}
+
+	void *symbol = dlsym(dlhandle_, "ipaCreate");
+	if (!symbol) {
+		LOG(IPAModule, Error) << "Failed to load ipaCreate() from IPA module shared object"
+				      << dlerror();
+		dlclose(dlhandle_);
+		return false;
+	}
+
+	ipaCreate_ = (ipaiFactory)symbol;
+
+	loaded_ = true;
+
+	return true;
+}
+
+/**
+ * \brief Instantiate an IPAInterface
+ *
+ * After the IPAInterface implementation factory has been loaded (with load()),
+ * an instance can be created with this function.
+ *
+ * Calling this function on a module that has not yet been loaded, or an
+ * invalid module (as returned by load() and isValid(), respectively) is
+ * an error.
+ *
+ * \return the IPA implementation as a new IPAInterface instance
+ */
+std::unique_ptr<IPAInterface> IPAModule::createInstance()
+{
+	if (!valid_ || !loaded_)
+		return nullptr;
+
+	std::unique_ptr<IPAInterface> ipai(ipaCreate_());
+
+	return ipai;
+}
+
 } /* namespace libcamera */
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index 07335e5..32f7da4 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -65,7 +65,8 @@ libcamera = shared_library('camera',
                            libcamera_sources,
                            install : true,
                            include_directories : includes,
-                           dependencies : libudev)
+                           dependencies : libudev,
+                           link_args : '-ldl')
 
 libcamera_dep = declare_dependency(sources : [libcamera_api, libcamera_h],
                                    include_directories : libcamera_includes,
