Design Patterns

Reviewed & published by Brayan K

By the end of this lesson you'll be able to recognise five classic design patterns — Singleton, Factory, Strategy, Observer, and RAII — and write each one idiomatically in modern C++ using smart pointers, lambdas, and std::function instead of leak-prone raw pointers.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Design patterns are like standard fittings in plumbing. A plumber doesn't invent a new joint for every house — they reach for a known fitting (a T-junction, a valve, a trap) that solves a recurring problem in a way the next plumber will instantly recognise. A Factory is the valve that decides what flows out; a Strategy is the swappable nozzle; an Observer is the alarm that triggers when pressure changes; RAII is the automatic shut-off that closes the supply the moment you walk away. Learning the patterns means you stop reinventing fittings and start speaking a shared language with every C++ developer who reads your code.

1. Singleton — Exactly One, Reachable Anywhere

The Singleton guarantees a class has exactly one instance and gives global access to it. In C++11 and later the cleanest version is Meyer's Singleton: a static local variable inside a function. The compiler guarantees it is created once, on first use, and that creation is thread-safe. You make the constructor private and = delete the copy operations so nobody can clone it. Read this worked example, run it, and watch the constructor fire only once.

Use sparingly: a Singleton is global state in disguise. It makes code harder to test and creates hidden coupling. Reach for it only for truly single resources like a logger — otherwise prefer passing the object in as a parameter.

#include <iostream>
#include <string>
using namespace std;

// A "pattern" is just a named, reusable shape for a common problem.
// SINGLETON = "there must be exactly ONE of this, reachable from anywhere."

// Meyer's Singleton: the modern, thread-safe C++ way.
class Logger {
    string logFile;

    // Private constructor -> nobody outside can write "Logger l;".
    Logger(const string& file) : logFile(file) {
        cout << "Logger created for: " << file << endl;  // runs ONCE only
    }
public:
    // Delete copy + assignment so you can never duplicate the instance.
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    static Logger& instance() {
        // 'static' local: created the FIRST time this runs, kept forever.
        // C++11 guarantees this initialisation is thread-safe.
        static Logger inst("app.log");
        return inst;                        // always the SAME object
    }

    void log(const string& msg) {
        cout << "[LOG -> " << logFile << "] " << msg << endl;
    }
};

int main() {
    Logger::instance().log("Application started");   // builds Logger, then logs
    Logger::instance().log("Processing data...");    // reuses the same Logger

    Logger& a = Logger::instance();
    Logger& b = Logger::instance();
    // Both names point at one object, so their addresses are equal.
    cout << "Same instance? " << (&a == &b ? "yes" : "no") << endl;  // yes
    return 0;
}

// ✅ Expected output:
//    Logger created for: app.log
//    [LOG -> app.log] Application started
//    [LOG -> app.log] Processing data...
//    Same instance? yes

2. Factory — Create Without Naming the Type

The Factory pattern decouples creating an object from using it. Callers ask for something by name and get back a base-class pointer — here unique_ptr<Shape> — without ever mentioning the concrete class. Returning a unique_ptr is the key modern detail: the object owns itself and frees automatically, so there's no delete to forget. Read the worked example first.

#include <iostream>
#include <memory>
#include <string>
using namespace std;

// FACTORY = "ask for a thing by name; get back the right object, hidden behind
// a base-class pointer." The caller never names the concrete class.

// Abstract base ("interface"): pure virtual -> Shape cannot be built directly.
class Shape {
public:
    virtual void draw() const = 0;          // = 0 means "subclasses MUST define"
    virtual double area() const = 0;
    virtual ~Shape() = default;             // virtual dtor -> safe to delete via base*
};

class Circle : public Shape {
    double radius;
public:
    explicit Circle(double r) : radius(r) {}
    void draw() const override { cout << "Circle  r=" << radius << endl; }
    double area() const override { return 3.14159 * radius * radius; }
};

class Square : public Shape {
    double side;
public:
    explicit Square(double s) : side(s) {}
    void draw() const override { cout << "Square  s=" << side << endl; }
    double area() const override { return side * side; }
};

// The factory returns unique_ptr<Shape>: the object OWNS itself and is freed
// automatically. No 'new', no 'delete', no leaks.
unique_ptr<Shape> createShape(const string& type, double size) {
    if (type == "circle") return make_unique<Circle>(size);
    if (type == "square") return make_unique<Square>(size);
    throw invalid_argument("Unknown shape: " + type);
}

int main() {
    // Code below depends only on Shape, never on Circle/Square.
    auto a = createShape("circle", 5.0);
    auto b = createShape("square", 4.0);

    a->draw();  cout << "  area = " << a->area() << endl;  // Circle r=5 ... 78.5
    b->draw();  cout << "  area = " << b->area() << endl;  // Square s=4 ... 16
    return 0;   // a and b free themselves here
}

// ✅ Expected output:
//    Circle  r=5
//      area = 78.5397
//    Square  s=4
//      area = 16

Your turn. The factory below is almost complete — fill in the two blanks marked ___ so each branch returns the right animal on the heap, using the // 👉 hints.

#include <iostream>
#include <memory>
#include <string>
using namespace std;

class Animal {
public:
    virtual string speak() const = 0;
    virtual ~Animal() = default;
};
class Dog : public Animal { public: string speak() const override { return "Woof"; } };
class Cat : public Animal { public: string speak() const override { return "Meow"; } };

unique_ptr<Animal> createAnimal(const string& kind) {
    // 🎯 YOUR TURN — make the factory return the right animal.

    // 1) If kind == "dog", return a Dog made on the heap.
    if (kind == "dog") return ___;   // 👉 make_unique<Dog>()

    // 2) If kind == "cat", return a Cat made on the heap.
    if (kind == "cat") return ___;   // 👉 make_unique<Cat>()

    throw invalid_argument("Unknown: " + kind);
}

int main() {
    auto pet = createAnimal("dog");
    cout << pet->speak() << endl;

    // ✅ Expected output:
    //    Woof
    return 0;
}

3. Strategy — Swap the Algorithm at Runtime

The Strategy pattern wraps an algorithm behind a common interface so you can change it on the fly. Classic C++ used an abstract base class with one virtual method; modern C++ usually skips that and stores a std::function instead — then any callable with the right signature (a free function, a lambda, even a captured object) is a valid strategy. Read the worked example, then write your own strategies.

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;

// STRATEGY = "swap the ALGORITHM at runtime without changing the caller."
// In modern C++ the strategy is just a std::function -> any matching callable.
using SortStrategy = function<void(vector<int>&)>;

void ascending(vector<int>& v) {
    cout << "Sorting ascending\n";
    sort(v.begin(), v.end());
}
void descending(vector<int>& v) {
    cout << "Sorting descending\n";
    sort(v.begin(), v.end(), greater<int>());
}

class Sorter {
    SortStrategy strategy;                         // holds whatever you plug in
public:
    void setStrategy(SortStrategy s) { strategy = s; }
    void run(vector<int>& data) { if (strategy) strategy(data); }
};

void print(const vector<int>& v) {
    for (int x : v) cout << x << ' ';
    cout << '\n';
}

int main() {
    Sorter sorter;
    vector<int> data = {42, 17, 93, 5};

    sorter.setStrategy(ascending);                 // plug in one algorithm
    auto up = data; sorter.run(up); print(up);     // 5 17 42 93

    sorter.setStrategy(descending);                // swap it for another
    auto down = data; sorter.run(down); print(down); // 93 42 17 5

    // A lambda is a valid strategy too — no named function needed:
    sorter.setStrategy([](vector<int>& v){ v.clear(); cout << "Cleared\n"; });
    auto gone = data; sorter.run(gone); print(gone); // (empty line)
    return 0;
}

// ✅ Expected output:
//    Sorting ascending
//    5 17 42 93
//    Sorting descending
//    93 42 17 5
//    Cleared

Now you try. A Discount is a strategy that takes a price and returns a new price. Fill in the two blanks with lambdas using the hints:

#include <iostream>
#include <functional>
using namespace std;

// A discount strategy takes a price and returns the new price.
using Discount = function<double(double)>;

double checkout(double price, Discount apply) {
    return apply(price);
}

int main() {
    // 🎯 YOUR TURN — write two pricing strategies as lambdas.

    // 1) "tenPercentOff": return price * 0.9
    Discount tenPercentOff = ___;   // 👉 [](double p){ return p * 0.9; }

    // 2) "fiverOff": subtract 5 from the price
    Discount fiverOff = ___;        // 👉 [](double p){ return p - 5; }

    cout << "Sale:   " << checkout(50.0, tenPercentOff) << endl;
    cout << "Coupon: " << checkout(50.0, fiverOff) << endl;

    // ✅ Expected output:
    //    Sale:   45
    //    Coupon: 45
    return 0;
}

4. Observer — Tell Everyone Who Signed Up

The Observer pattern lets objects subscribe to changes in another object without the two being tightly coupled. The subject (here a PriceFeed) keeps a list of subscribers and notifies each one when something changes. Storing subscribers as std::function means each observer is just a lambda — no observer base class required.

Pro Tip: for thread-safe observers, guard the listener list with a std::mutex, and if observers can outlive or die before the subject, store std::weak_ptr so you never call into a destroyed object.

#include <iostream>
#include <vector>
#include <string>
#include <functional>
using namespace std;

// OBSERVER = "when something changes, tell everyone who signed up — without the
// subject knowing who they are." Modern C++ stores subscribers as std::function.
class PriceFeed {
    using Listener = function<void(double)>;
    vector<Listener> listeners;                    // the "observers"
public:
    void subscribe(Listener cb) { listeners.push_back(cb); }

    void setPrice(double price) {
        cout << "Price now $" << price << '\n';
        for (auto& notify : listeners) notify(price);   // tell every observer
    }
};

int main() {
    PriceFeed feed;

    // The dashboard just prints the new price.
    feed.subscribe([](double p){ cout << "  Dashboard: $" << p << '\n'; });

    // The alert only reacts above a threshold.
    feed.subscribe([](double p){
        if (p > 100) cout << "  ALERT: above $100!\n";
    });

    feed.setPrice(85);    // both observers run; alert stays quiet
    feed.setPrice(120);   // both run; alert fires
    return 0;
}

// ✅ Expected output:
//    Price now $85
//      Dashboard: $85
//    Price now $120
//      Dashboard: $120
//      ALERT: above $100!

5. RAII — The Pattern That Powers the Rest

RAII (Resource Acquisition Is Initialisation) is the most important pattern in C++, and it's the one beginners overlook because it has no Gang-of-Four name. The idea: tie a resource's lifetime to an object. Acquire it in the constructor, release it in the destructor, and the language guarantees the destructor runs when the object leaves scope — even if an exception is thrown. Smart pointers are RAII for memory; the same pattern handles files, locks, and sockets.

#include <iostream>
#include <string>
using namespace std;

// RAII = "Resource Acquisition Is Initialisation." The pattern: tie a resource's
// lifetime to an object. Acquire in the constructor, release in the destructor.
// When the object leaves scope, cleanup happens AUTOMATICALLY — even if an
// exception is thrown. This is the most important "pattern" in all of C++.
class FileGuard {
    string name;
public:
    explicit FileGuard(const string& n) : name(n) {
        cout << "OPEN  " << name << '\n';     // acquire the resource
    }
    ~FileGuard() {
        cout << "CLOSE " << name << '\n';     // release it, guaranteed
    }
};

void process() {
    FileGuard f("data.txt");                  // OPEN happens here
    cout << "  ...working with data.txt\n";
}                                             // CLOSE happens HERE, on scope exit

int main() {
    process();
    cout << "Done — file was closed for us.\n";
    // Smart pointers ARE RAII: unique_ptr frees memory in its destructor the
    // exact same way FileGuard closes the file.
    return 0;
}

// ✅ Expected output:
//    OPEN  data.txt
//      ...working with data.txt
//    CLOSE data.txt
//    Done — file was closed for us.

Common Errors (and the fix)

📋 Quick Reference

PatternUse it when…Modern C++ idiom
SingletonYou need exactly one shared instance (logger, config)static local in instance()
FactoryCreation should be decoupled from useunique_ptr + make_unique
StrategyAn algorithm must be swappable at runtimestd::function + lambda
ObserverMany objects react to one object's changesvector of std::function
RAIIA resource must be released no matter whatctor acquires / dtor releases

Mini-Challenge: a Notifier Factory

No blanks this time — just a brief and an outline. Combine the Factory pattern with a polymorphic interface to build it from scratch, then run it and check your output against the example in the comments.

#include <iostream>
#include <memory>
#include <string>
using namespace std;

class Notifier {
public:
    virtual void send(const string& msg) const = 0;
    virtual ~Notifier() = default;
};

int main() {
    // 🎯 MINI-CHALLENGE: a notifier Factory
    // 1. Write two classes that inherit Notifier: EmailNotifier and SmsNotifier.
    //    Each overrides send() to print e.g.  "EMAIL: <msg>"  /  "SMS: <msg>".
    // 2. Write  unique_ptr<Notifier> makeNotifier(const string& kind)
    //    that returns the right one ("email" / "sms"), like createShape() earlier.
    // 3. In main, make an "email" notifier and call send("Order shipped").
    //
    // ✅ Example output (kind = "email"):
    //    EMAIL: Order shipped

    // your code here
    return 0;
}

🎉 Lesson Complete

Practice quiz

What does the Singleton pattern guarantee?

  • A class can never be instantiated
  • Every object is automatically copied
  • A class has exactly one instance, reachable from anywhere
  • A class always returns a new object on each call

Answer: A class has exactly one instance, reachable from anywhere. Singleton ensures exactly one instance with global access — useful for a logger or config, but use it sparingly.

How does Meyer's Singleton create its single, thread-safe instance in modern C++?

  • A static local variable inside a function (instance())
  • A global variable at file scope
  • A new call in the constructor
  • A shared_ptr passed everywhere

Answer: A static local variable inside a function (instance()). A static local inside instance() is created once on first use, and C++11 guarantees that initialisation is thread-safe.

Why does a modern Factory return unique_ptr<Shape> instead of a raw pointer?

  • Raw pointers cannot point to derived classes
  • unique_ptr is faster to construct
  • It is required by the Shape base class
  • unique_ptr owns the object and frees it automatically, so there is no delete to forget (no leaks)

Answer: unique_ptr owns the object and frees it automatically, so there is no delete to forget (no leaks). Returning unique_ptr makes ownership explicit and cleanup automatic, so a forgotten delete can never leak.

In modern C++, the Strategy pattern is usually implemented by storing what?

  • A raw function pointer array
  • A std::function that holds any matching callable
  • A global flag
  • A vector of integers

Answer: A std::function that holds any matching callable. Strategy stores a std::function, so any matching callable — a free function, lambda, or functor — is a valid strategy.

What does the Observer pattern let you do?

  • Notify all subscribers when a subject changes, without the subject knowing who they are
  • Guarantee one instance of a class
  • Swap an algorithm at runtime
  • Free memory automatically

Answer: Notify all subscribers when a subject changes, without the subject knowing who they are. Observer keeps a list of subscribers (often std::function) and notifies each one on a change, keeping them decoupled.

What is the core idea of RAII?

  • Allocate all memory at program start
  • Never use destructors
  • Tie a resource's lifetime to an object: acquire in the constructor, release in the destructor
  • Always call close() manually

Answer: Tie a resource's lifetime to an object: acquire in the constructor, release in the destructor. RAII acquires a resource in the constructor and releases it in the destructor, so cleanup is automatic and exception-safe.

Why must a polymorphic base class like Shape have a virtual destructor?

  • To make the class abstract
  • Deleting a derived object through a base pointer without it is undefined behaviour and leaks the derived part
  • To allow copying
  • Virtual destructors are never needed

Answer: Deleting a derived object through a base pointer without it is undefined behaviour and leaks the derived part. Without virtual ~Shape(), deleting a derived object via Shape* is undefined behaviour and the derived part leaks.

How do you stop a Singleton from being duplicated?

  • Make the constructor public
  • Mark the class final
  • Use a global variable
  • = delete the copy constructor and copy assignment operator

Answer: = delete the copy constructor and copy assignment operator. Deleting the copy constructor and assignment (and making the constructor private) ensures nobody can clone the instance.

When should you prefer std::function over a virtual interface for a behaviour?

  • When the behaviour has many related methods and its own state
  • When the strategy or callback is small and you want maximum flexibility with no class to write
  • Never; interfaces are always better
  • Only for memory management

Answer: When the strategy or callback is small and you want maximum flexibility with no class to write. std::function shines for small one-method strategies and callbacks; a virtual interface fits richer, multi-method contracts.

What is a 'fat interface' and how do you fix it?

  • An interface with one method; merge it with others
  • A class that uses too much memory; shrink its fields
  • A base class with many unrelated pure-virtual methods; split it into small focused interfaces (Interface Segregation)
  • An interface with a virtual destructor; remove it

Answer: A base class with many unrelated pure-virtual methods; split it into small focused interfaces (Interface Segregation). A fat interface forces empty or throw-only overrides; the fix is to split it into several small, focused interfaces.

Continue this course

Frequently asked questions

Is the Singleton pattern an anti-pattern I should avoid?

Not always, but treat it with suspicion. A Singleton is hidden global state: it makes code harder to test (you cannot swap in a fake) and creates surprising coupling. Use it only for genuinely single, global resources like a logger or configuration store. If you find yourself reaching for it often, prefer passing the object in as a parameter (dependency injection) instead.

Why do the factory examples return unique_ptr instead of a raw pointer?

A raw owning pointer (returning 'new Circle(...)') makes the caller responsible for calling delete, and one forgotten delete is a memory leak. unique_ptr owns the object and frees it automatically when it goes out of scope, so leaks become impossible. Returning unique_ptr<Shape> is the modern idiom for factories.

When should I use std::function and when should I use a virtual interface?

Use std::function (with lambdas) when the strategy or callback is small and you want maximum flexibility — any matching callable works, with no class to write. Use a virtual interface when the behaviour has multiple related methods, needs its own state, or forms a stable contract many classes implement. std::function shines for one-method strategies and observers; interfaces shine for richer abstractions.

Do I still need RAII if I use smart pointers everywhere?

Smart pointers ARE RAII — unique_ptr and shared_ptr free memory in their destructors. But RAII applies to every resource, not just memory: open files, locked mutexes, network sockets, database handles. Wrapping each in a small RAII guard (like std::lock_guard for mutexes) means cleanup is automatic and exception-safe, with no manual close() to forget.

What is a 'fat interface' and why is it a problem?

A fat interface is a base class that piles on many unrelated pure-virtual methods, forcing every subclass to implement things it does not need. It leads to empty or throw-only overrides and brittle code. The fix is the Interface Segregation Principle: split it into several small, focused interfaces so a class only implements what it actually uses.

Related lessons