bigonotes
CS ConceptsDesign Patterns

What Are Design Patterns?

What design patterns are, why they matter, and how the creational, structural and behavioral families differ.

6 min readLast updated Design Patterns

A design pattern is a reusable, proven solution to a problem that keeps showing up in software design. It is not code you copy and paste. It is a blueprint you adapt to your situation, and a shared name your team can use to talk about the design. The 23 classic patterns in this section come from the Gang of Four (GoF) book, Design Patterns: Elements of Reusable Object-Oriented Software (1994).

In the interview: You won't have time to implement a pattern in full. Name the pattern that fits the scenario, say why in a sentence, and move on. Study the patterns to understand why they exist and what problem each one solves — there's no need to memorize the templates.

Right before an interview: Brush up on the Pattern Reference Cards and Choosing the Right Pattern, at the bottom of this list.

What a pattern is (and isn't)#

Architects don't reinvent stairs every time they need to connect two floors. They reuse a known solution and adapt it: spiral stairs, straight stairs, an elevator. Software has the same kind of repeating problems:

  • "How do we make sure only one connection pool exists?"
  • "How do we notify several parts of the program when something changes?"
  • "How do we add features to an object without modifying its class?"
A pattern is not A pattern is
Code you copy A template you adapt
A library or framework you import A shared vocabulary between developers
A rule you must follow Distilled experience with known trade-offs

Why patterns help#

Without patterns, every developer solves the same problem differently:

# Developer A: hard-coded notifications
class NewsSystem:
    def something_happened(self) -> None:
        email_system.send()
        sms_system.send()
        push_notification.send()
        # Adding a new channel means editing this method


# Developer B: a different, ad-hoc approach to the same problem
class EventManager:
    def __init__(self):
        self.subscribers: list[str] = []

    def notify_all_subscribers(self) -> None:
        for sub in self.subscribers:
            ...  # inconsistent with Developer A's design

This causes three problems:

  1. Reinventing the wheel. Each home-grown solution has to rediscover the same edge cases (for example, a subscriber unsubscribing in the middle of a notification loop).
  2. Communication barriers. "I built a callback mechanism with a registry..." takes a paragraph. "I used the Observer pattern" takes one sentence.
  3. Unproven designs. Custom solutions haven't been exercised across thousands of codebases.

With patterns you get:

  • Proven solutions whose trade-offs are documented.
  • A common vocabulary. "Singleton for the config" replaces a long explanation about controlling instance creation and a shared accessor.
  • Faster problem solving. Recognize the problem, pick the pattern, adapt it.
  • Better structure. Code that new teammates can recognize and navigate.

The three families#

The Gang of Four (GoF) grouped their 23 patterns into three families, based on what kind of problem they solve.

Creational: how objects are created#

Creating an object looks trivial (Thing()), until you need exactly one instance, don't know the concrete type until runtime, need many construction steps, or want to copy an existing object instead of building a new one.

Pattern One line
Singleton Only one instance ever exists
Factory Method Let subclasses decide which object to create
Abstract Factory Create families of related objects
Builder Build complex objects step by step
Prototype Create objects by cloning an existing one
# Without a pattern: creation logic repeated wherever you need one
if kind == "email":
    notif = EmailNotification(recipient, subject, body, smtp_server)
elif kind == "sms":
    notif = SMSNotification(phone_number, message, gateway)
elif kind == "push":
    notif = PushNotification(device_id, title, message, icon)

# With a factory: creation lives in one place
notif = NotificationFactory.create(kind, params)

Structural: how objects are composed#

These patterns are about making pieces fit together: connecting incompatible interfaces, layering features onto objects, hiding complex subsystems, and sharing resources.

Pattern One line
Adapter Make incompatible interfaces work together
Decorator Add behavior to an object without modifying it
Facade Give a complex system a simple interface
Composite Treat single objects and groups the same way
Flyweight Share common state to save memory
Bridge Separate an abstraction from its implementation
Proxy A stand-in that controls access to another object
# Without a pattern: one subclass per combination
class CoffeeWithMilk(Coffee): ...
class CoffeeWithSugar(Coffee): ...
class CoffeeWithMilkAndSugar(Coffee): ...
class CoffeeWithMilkAndSugarAndWhip(Coffee): ...  # class explosion

# With Decorator: combine layers at runtime
coffee = BasicCoffee()
coffee = MilkDecorator(coffee)
coffee = SugarDecorator(coffee)
coffee = WhipDecorator(coffee)

Behavioral: how objects communicate#

These patterns assign responsibilities and define how objects talk: notifying others about changes, changing behavior with state, swapping algorithms, passing requests along a chain.

Pattern One line
Strategy Swap algorithms at runtime
Observer Notify many objects when one changes
Command Wrap a request in an object
State Change behavior when internal state changes
Template Method Fix an algorithm's skeleton, let subclasses fill in steps
Iterator Walk a collection without exposing its internals
Chain of Responsibility Pass a request along a chain until someone handles it
# Without a pattern: every payment type in one method
class Checkout:
    def process_payment(self, kind: str, amount: float) -> None:
        if kind == "creditcard":
            ...  # 50 lines of card processing
        elif kind == "paypal":
            ...  # 30 lines of PayPal processing
        elif kind == "crypto":
            ...  # 40 lines of crypto processing


# With Strategy: the algorithm is a pluggable object
class StrategyCheckout:
    def __init__(self, strategy: "PaymentStrategy"):
        self._strategy = strategy

    def process_payment(self, amount: float) -> None:
        self._strategy.pay(amount)

Structural vs behavioral: the confusing part#

Creational is easy to spot. Structural and behavioral get mixed up, because both often involve one object holding a reference to another.

Structural Behavioral
Focus How pieces are assembled How pieces interact and share responsibility
Mental picture A photograph (static architecture) A movie (dynamic interaction)
Key question "How is this built?" "How do these talk / act?"
Examples Adapter wraps, Decorator layers, Facade hides Observer notifies, Strategy swaps, Chain passes along
# STRUCTURAL: Facade hides several classes behind one simple method
class ComputerFacade:
    def __init__(self, cpu: "CPU", memory: "Memory", hd: "HardDrive"):
        self._cpu = cpu
        self._memory = memory
        self._hd = hd

    def start(self) -> None:
        self._cpu.freeze()
        self._memory.load()
        self._hd.read()


# BEHAVIORAL: Observer defines who gets told when state changes
subject.attach(observer1)
subject.attach(observer2)
subject.set_state("new")  # both observers are notified

The same domain can call for either. Switching databases:

# Adapter (structural): make MySQL's API fit *our* interface
class MySQLAdapter(DatabaseInterface):
    def __init__(self, mysql: "MySQLDatabase"):
        self._mysql = mysql

    def query(self) -> None:
        self._mysql.execute_query()  # translate the call


# Strategy (behavioral): choose *how* data is saved at runtime
class DataService:
    def __init__(self, strategy: "DatabaseStrategy"):
        self._strategy = strategy

    def save(self, data: "Data") -> None:
        self._strategy.save(data)
  • Adapter: "I need this square peg to fit a round hole." That's structure.
  • Strategy: "I need to switch between different ways of saving." That's behavior.

Patterns in low-level design interviews#

In LLD interviews (design a parking lot, a ticket-booking system, a ride-hailing app) patterns give you:

  • Vocabulary. "I'll use Strategy for pricing" communicates your approach instantly.
  • Structure. A proven class layout to start from.
  • Credibility. It shows you think about change and extension, not just "it works".

Interviewers don't expect you to recite all 23. They expect you to recognize which problem calls for which pattern. A sensible learning order:

  1. Must know: Singleton, Factory, Strategy, Observer
  2. Good to know: Builder, Decorator, Adapter, Facade
  3. Bonus: Command, State, Template Method, Prototype, Composite, Flyweight, Bridge

Key takeaways#

  • A pattern is a blueprint plus a name, not a library or copy-paste code.
  • Always understand the problem a pattern solves before learning its solution.
  • Creational = object creation, Structural = composition, Behavioral = communication.
  • Structural is about how things are built; behavioral is about how they act at runtime.
  • You need about 8 patterns well for interviews; recognizing when to use them matters more than memorizing all 23.

Found a mistake? Tell me on LinkedIn ↗

Further reading#