Factory Design Pattern in C++

During software/firmware development, one of the most important principles to follow is loose coupling. It is keeping different parts of a system as independent as possible. This allows your codebase to evolve, scale, and adapt to new requirements with minimal friction.

One effective way to achieve loose coupling is by abstracting the object creation process. Instead of letting client code directly instantiate objects, we delegate that responsibility to a dedicated component. This is where the Factory Design Pattern comes into play.

In this blog post, you will learn:

  • What is Factory Design Pattern?
  • Real-world applications of the Factory Design Pattern.
  • How it fits into embedded system design?
  • Best practices for implementing it in C++

Let’s dive in and see how this pattern can help you write cleaner, more maintainable, and more scalable code.

What is Factory Design Pattern?

The Factory Pattern is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.

That means, instead of creating objects directly, the Factory Pattern delegates this responsibility to a factory method. This encapsulates the instantiation logic, enables runtime decision-making, and leads to more flexible and scalable code.

I believe, you might be wondering: why is this useful?

Don’t worry your answer is below.

One of its key advantages is the decoupling of client code from concrete implementations, which enhances testability, maintainability, and adaptability. This is especially valuable in embedded firmware development, where hardware platforms, drivers, and communication protocols frequently vary across projects or product lines.

In one of my projects, I used the Factory Pattern in the bootloader code to manage the creation of objects of bootloader class for different situation. I will explain this latter part of this article.

 

😞 Problem:

Let’s consider a real-life scenario which commonly encountered in embedded systems design.

Suppose you are developing a temperature monitoring system for a modular firmware platform. Your code is part of a modular platform, that use by different products. That means it has to support multiple sensor variants, including:

  • LM75 – I²C-based temperature sensor.
  • TMP102 – I²C sensor with alternative address/configuration.
  • DS18B20 – 1-Wire digital temperature sensor.

In future might be this list increase to reduce to product cost or to increase the performance. That means your firmware must be flexible enough to select the appropriate sensor at runtime or build time, depending on:

  • Configuration values stored in EEPROM or Flash.
  • User input from a configuration interface.
  • Compile-time flags for preprocessor-based selection.

I believe most of engineers take the straightforward approach and they create sensor objects directly within the application code based on the sensor type, like this:

if (sensorType == "LM75")
{
    TempSensor* pSensor = new LM75Sensor();
}
else if (sensorType == "TMP102")
{
    TempSensor* pSensor = new TMP102Sensor();
}
else if (sensorType == "DS18B20")
{
    TempSensor* pSensor = new DS18B20Sensor();
}

However, implementing this kind of object creation directly within your application logic can cause several serious problems:

Tight Coupling to Concrete Classes:

The application becomes dependent on specific sensor implementations, violating the Open/Closed Principle and making future changes risky and error prone.

Violation of Separation of Concerns:

Mixing object creation with business logic breaks the Single Responsibility Principle, reducing code clarity and maintainability.

Limited Testability:

Hardcoded dependencies make it difficult to mock components, complicating unit testing and hindering automated validation.

Poor Scalability and Extensibility:

Supporting new sensor types requires modifying existing logic, which doesn’t scale well as the system grows.

 

Now let’s look at the solution to the above problem, which becomes messy over time as the project grows, and more sensor variants are added.

✅ Solution: How Factory Pattern Solves It:

The Factory Pattern promotes replacing direct object creation (i.e., using new) with calls to a dedicated factory method. Internally, the factory still uses the same approach to create the object (new to create objects), but the instantiation is encapsulated, keeping it hidden from the client code. The returned instances, commonly referred to as products, are accessed through a common interface or base class.

At first, it might look like we are just moving the new keyword from one place to another. But the real benefit is flexibility. By using a factory method, we can let subclasses decide which type of object to create. This means we can change the kind of object being made without changing the code (client code) that uses it.

But here is one small limitation: the different types of objects (products) that subclasses return must all share a common base class or interface. This is important so that the factory method can return them using the same type. In other words, the return type of the factory method in the base class should be the shared base class or interface.

So, at the end with just this small changes. We achieved the following things.

Centralizes Object Creation

All instantiation logic resides in one place, making it easier to manage and modify.

Encapsulates Complex Construction Logic

Clients are decoupled from how objects are created. This abstraction hides details like configuration, dependencies, or hardware-specific initialization.

Supports the Open/Closed Principle (OCP)

New product types can be introduced by extending the factory without modifying existing code — keeping your codebase more stable and less error-prone.

Enables Runtime Selection of Implementations

Choose which class to instantiate based on configuration, environment, or hardware — all at runtime.

Improves Testability

Through polymorphism and dependency injection, factories enable seamless substitution of mocks or stubs during unit testing.

Enhances Maintainability and Portability

Especially useful in systems where you support multiple variants (e.g., different hardware or platforms). The factory isolates variant-specific code.

 

How to Implement Factory Pattern in C++:

To understand how the Factory Method Pattern helps solve this problem, let’s consider a above real-world scenario. Where you need to support multiple temperature sensors — for example:

  • LM75
  • TMP102
  • DS18B20

All of these temperature sensors behave differently internally, but they share a common purpose that is measuring temperature. So, let’s see the steps to how we can make the code clean and flexible with the help of factory pattern.

Step 1: Define an Abstract Interface:

Implement an abstract or interface class which shared by all factory products. This interface should declare methods that make sense in every product. So below I have created an interface class.

/**
 * @brief Interface for temperature sensors.
 *
 * All temperature sensor drivers (e.g., LM75, TMP102, DS18B20) must implement this interface.
 * It provides a common way to read temperature values and identify the sensor.
 */
class ITemperatureSensor
{
public:
    /**
     * @brief Reads the current temperature from the sensor.
     *
     * @return Temperature in degrees Celsius.
     */
    virtual float readTemperature() = 0;

    /**
     * @brief Gets the name or identifier of the sensor.
     *
     * @return A C-string representing the sensor's name (e.g., "LM75", "TMP102").
     */
    virtual const char* getSensorName() const = 0;

    /**
     * @brief Virtual destructor to ensure proper cleanup of derived sensor classes.
     */
    virtual ~ITemperatureSensor() = default;
};

 

Step 2: Implement Concrete Products:

In below code you can see all classes are derived from the ITemperatureSensor.

class LM75Sensor : public ITemperatureSensor
{
public:
    float readTemperature() override
    {
        return 24.5f;
    }
    const char* getSensorName() const override
    {
        return "LM75";
    }
};



class TMP102Sensor : public ITemperatureSensor
{
public:
    float readTemperature() override
    {
        return 25.2f;
    }
    const char* getSensorName() const override
    {
        return "TMP102";
    }
};

Step 3: Create a Factory:

Now, we will define a factory class with a static method responsible for creating temperature sensor objects based on the selected enum type. This static method encapsulates all the object creation logic. The return type of this static method should match the common product interface.

/**
 * @brief Enumeration of supported temperature sensor types.
 */
enum class ESensorType
{
    LM75 = 0,
    TMP102 = 1,
};

/**
 * @brief Factory class to create instances of temperature 
 *        sensors based on sensor type.
 */
class SensorFactory
{
public:
    /**
     * @brief Creates a temperature sensor instance based on the specified type.
     *
     * @param type The sensor type (e.g., LM75, TMP102).
     * @return A pointer to a newly created ITemperatureSensor object. 
     *         Returns nullptr if type is unsupported.
     *         Caller is responsible for deleting the returned pointer.
     */
    static ITemperatureSensor* createSensor(ESensorType type)
    {
        switch (type)
        {
        case ESensorType::LM75:
        {
            return new LM75Sensor();
        }

        case ESensorType::TMP102:
        {
            return new TMP102Sensor();
        }

        default:
        {
            return nullptr;
        }
        }
    }
};

 

Step 4: Usage of factory method in Client Code:

In the below client code, I have used the factory method to create the sensor object. This client does not know or care which specific sensor is being used. This design allows us to add new sensor types (like DS18B20) seamlessly without touching this client code.

void runSensorClient()
{
    //Get Sensor type
    ESensorType configType = readSensorTypeFromConfig();

    ITemperatureSensor* pSensor = SensorFactory::createSensor(configType);

    if (pSensor)
    {
        printf("Using sensor: %s\n", pSensor->getSensorName());
        printf("Temperature: %.2f °C\n", pSensor->readTemperature());

        delete pSensor;  // Cleanup
    }
    else
    {
        printf("Error: Unsupported sensor type configured.\n");
    }
}

 

The Factory Method separates product construction code from the code that actually uses the product. Therefore, it is easier to extend the construction logic independently.

 

🚨 Very important:

  • Avoid new operator in real embedded systems.
  • Replace dynamic allocation with:
    • Static objects.
    • Pre-allocated buffers.
    • Placement new with memory pools.
  • We can replace switch-case with function pointer tables or static maps in factory method.

 

🎯 When to Use the Factory Pattern:

There are following scenario to use the factory method in your project.

1. Use the Factory Pattern when you don’t know which type of object you need until the program runs.

In some embedded systems, the type of temperature sensor (like LM75, TMP102, or another) might not be known at compile time. It could be selected based on:

  • Settings stored in EEPROM.
  • Board configuration.
  • User selection.
  • Sensor detected at startup

The Factory Pattern helps in this context.

 

2. Use it when you want to keep object creation separate from object usage:

It is a good practice to separate object creation logic from application logic. Your application code should focus only on reading the temperature from the sensor. It should not need to know which specific sensor is being used.

By using a factory, your application interacts only with a common interface, such as ITemperatureSensor. The factory is responsible for selecting and instantiating the appropriate sensor class (LM75Sensor, TMP102Sensor, etc.).

This means that when you add a new sensor (e.g., DS18B20), you only need to update the factory. The rest of the application remains unchanged — making your codebase easier to maintain, extend, and test.

 

3. Use it to let others extend your code easily:

If you’re writing a firmware library or SDK that others will use, they might want to use their own sensor.

With the factory method:

  • They can create a new sensor class (e.g., CustomSensor) that follows your ITemperatureSensor interface.
  • Then they can change or override the factory to return their custom sensor.

This way, your code stays the same, and others can plug in their own sensors easily.

 

4. Use the Factory Pattern when you want to reuse the same object instead of creating it again and again:

In embedded systems, creating new objects repeatedly can waste memory or slow things down.

If you only need one sensor instance, your factory can:

  • Check if the sensor object already exists.
  • If it exists, return it.
  • If not, create it, save it, and return it.

This reuse logic cannot be handled by a constructor, because constructors always create a new object when called. In contrast, a factory method gives you full control over object creation allowing you to return an existing instance instead of creating a new one.

That is exactly what the factory pattern is designed for: managing and abstracting object creation, whether it’s returning a new instance or reusing an existing one.

 

👍 Pros and 👎 Cons of Factory Design Pattern:

✅ Pros:

  • Scalable: Easy to add new products without changing client code
  • Decoupled: Separation between object creation and usage
  • Centralized logic: All object instantiation happens in one place
  • Testability: Easier to inject mocks for unit testing
  • Runtime flexibility: Useful for embedded config stored in flash, EEPROM

❌ Cons

  • Indirection: Slightly harder to trace object creation
  • More Classes: Adds overhead in terms of file and class count
  • Memory: Care needed in embedded systems with limited heap; prefer static allocation
  • Overkill for simple cases: For 1-2 object types, a switch may suffice

Relations with Other Patterns: