DEV Community

Deepika Pusala
Deepika Pusala

Posted on

Structural & Behavioral Design Patterns in Python: Repository, Factory, Strategy, Decorator & Adapter Explained ๐Ÿงฉ

๐Ÿงฉ Structural & Behavioral Design Patterns: Let's Learn from ZERO!

Design patterns teach you how to organize real-world application code so it is:

  • ๐Ÿ“– Easy to understand
  • ๐Ÿ› ๏ธ Easy to maintain
  • ๐Ÿงช Easy to test
  • โž• Easy to extend with new features
  • ๐Ÿ”— Less tightly coupled
  • โ™ป๏ธ Reusable across different parts of an application

1. ๐Ÿค” First, What Are Design Patterns?

Imagine you're building an Instagram-like app with login, creating posts, fetching posts, likes, image uploads, notifications and follows. At first you might write everything in one function:

def create_post():
    # Validate user
    # Check authentication
    # Save post to database
    # Upload image
    # Send notification
    # Return response
    pass
Enter fullscreen mode Exit fullscreen mode

It works at first. But when the app grows to 100 features:

  • ๐Ÿ’ฅ Changing one feature might break another
  • ๐Ÿงช Testing becomes difficult
  • ๐Ÿ” Code becomes duplicated
  • ๐Ÿงถ Database logic gets mixed with business logic
  • ๐Ÿ˜ฉ Adding new functionality becomes complicated

๐Ÿ“Œ A design pattern is a reusable solution or blueprint for a commonly occurring software design problem.

โš ๏ธ A design pattern is NOT a library, framework, or ready-made code you copy. It's an approach to organizing your code.

1.1 ๐Ÿ—‚๏ธ Three Categories of Design Patterns

Category Concerned with Examples
๐Ÿญ Creational How objects are created Factory, Singleton, Builder
๐Ÿงฑ Structural How classes and objects are organized and connected Repository, Adapter, Decorator, Facade
๐ŸŽญ Behavioral How objects communicate and distribute responsibilities Strategy, Observer, Command, State

Our focus today:

Pattern Category Main purpose
Repository Architectural / structural Separates database operations
Factory Creational Creates objects
Strategy Behavioral Changes algorithms easily
Decorator Structural Adds functionality without changing original code
Adapter Structural Makes incompatible interfaces work together

๐Ÿ“ Repository is often called a design pattern, but it's more specifically a data-access architectural pattern. Factory is creational, while Strategy is behavioral.


2. ๐Ÿงน Separation of Concerns (SoC)

Before individual patterns, one foundational principle. Think of a restaurant ๐Ÿฝ๏ธ: the waiter takes orders, the chef prepares food, the cashier handles payments, the storekeeper manages ingredients. Would you ask one person to do everything? Probably not. Software should work the same way.

๐Ÿ“Œ Separation of Concerns means dividing an application into different parts, where each part handles a specific responsibility.

In a social-feed backend:

Component Responsibility
๐Ÿ›ฃ๏ธ Controller / Router Handles HTTP requests
๐Ÿง  Service Contains business logic
๐Ÿ“š Repository Communicates with the database
๐Ÿ—„๏ธ Model Represents database structure
โœ”๏ธ Schema Validates request and response data

A user creates a post:

โš›๏ธ Frontend (React)       โ†’ user clicks Create Post
        โ–ผ
๐Ÿ›ฃ๏ธ Controller / Router    โ†’ receives the HTTP POST request
        โ–ผ
๐Ÿง  Service                โ†’ checks business rules, decides what should happen
        โ–ผ
๐Ÿ“š Repository             โ†’ performs database operations
        โ–ผ
๐Ÿ˜ PostgreSQL             โ†’ stores the post
Enter fullscreen mode Exit fullscreen mode

This is called Layered Architecture.


3. ๐Ÿฅž Layered Architecture: Controller โ†’ Service โ†’ Repository

Layered architecture divides an app into layers, each with its own responsibility. The most common backend structure is Controller (or Router) โ†’ Service โ†’ Repository โ†’ Database.

3.1 ๐Ÿ›ฃ๏ธ Controller Layer

๐Ÿ“Œ A controller is the entry point through which an external client communicates with the backend.

In FastAPI we generally use APIRouter and route functions:

from fastapi import APIRouter, Depends

router = APIRouter()


@router.post("/posts")
def create_post(post_data: PostCreate):
    return post_service.create_post(post_data)
Enter fullscreen mode Exit fullscreen mode
  1. React sends a POST request
  2. FastAPI receives it
  3. The controller gets the request data
  4. It calls the service
  5. It returns the service result

The controller should NOT contain all business logic or SQL queries. Think of it as a receptionist ๐Ÿ›Ž๏ธ: it receives your request and sends it to the right department, without personally doing every job.

3.2 ๐Ÿง  Service Layer

The service layer contains the app's business logic, the actual rules that define how your app behaves. In Instagram, for example:

  • A user cannot create a post without authentication
  • A user cannot edit another person's private post
  • A post must have valid content or media
  • A user cannot like the same post twice
class PostService:
    def __init__(self, post_repository):
        self.post_repository = post_repository

    def create_post(self, user_id, content):
        if not content.strip():
            raise ValueError("Post cannot be empty")

        post = self.post_repository.create(
            user_id=user_id,
            content=content,
        )

        return post
Enter fullscreen mode Exit fullscreen mode
  • PostService handles post-related business logic
  • __init__() receives the repository
  • create_post() checks whether content is empty
  • If valid, it asks the repository to save the post

Notice: the service does not write SQL. It delegates database work to the repository.

3.3 ๐Ÿ“š Repository Layer

๐Ÿ“Œ A repository handles communication between your application and the database. It sits between business logic and data storage.

class PostRepository:
    def __init__(self, db):
        self.db = db

    def create(self, user_id, content):
        post = Post(
            user_id=user_id,
            content=content,
        )

        self.db.add(post)
        self.db.commit()
        self.db.refresh(post)

        return post
Enter fullscreen mode Exit fullscreen mode
  1. Create a Post database object
  2. Add it to the database session
  3. Commit the transaction
  4. Refresh the object to retrieve updated database values
  5. Return the saved post

3.4 ๐Ÿค” Why separate these three layers?

Imagine one function doing everything:

@router.post("/posts")
def create_post(data: PostCreate):
    # Validation
    # Authentication
    # Business rules
    # SQL queries
    # Database commit
    # Response formatting
    pass
Enter fullscreen mode Exit fullscreen mode

That's tightly coupled code. If you change your database and database code is scattered everywhere, you modify many functions. With layered architecture, database changes are concentrated mainly in the repository and persistence configuration.

Controller Service Repository
Handles HTTP Handles business rules Handles data access
Receives requests Makes decisions Executes queries
Calls service Calls repository Talks to database
Returns responses Returns business results Returns stored data

๐ŸŽค Interview question: Why shouldn't controllers directly communicate with databases?

Answer: It mixes HTTP handling, business logic and data-access responsibilities. Separating them improves maintainability, testability and reusability, and reduces coupling.


4. ๐Ÿ“š Repository Pattern

Structural / data-access pattern

4.1 ๐Ÿšจ The problem

Without the Repository Pattern:

@router.get("/posts")
def get_posts(db: Session):
    posts = db.query(Post).all()
    return posts
Enter fullscreen mode Exit fullscreen mode

The controller talks directly to the database. With 20 endpoints needing database access, you'd repeat database operations everywhere, causing duplication and tight coupling.

4.2 โœ… The solution

Create a separate repository class that handles all database operations for posts.

โŒ Without Repository Pattern:  Controller โ”€โ”€โ–ถ Database
โœ… With Repository Pattern:     Controller โ”€โ”€โ–ถ Service โ”€โ”€โ–ถ PostRepository โ”€โ”€โ–ถ Database
Enter fullscreen mode Exit fullscreen mode

4.3 ๐Ÿงต Full example

The repository:

class PostRepository:
    def __init__(self, db):
        self.db = db

    def get_all(self):
        return self.db.query(Post).all()

    def get_by_id(self, post_id):
        return (
            self.db.query(Post)
            .filter(Post.id == post_id)
            .first()
        )

    def create(self, post):
        self.db.add(post)
        self.db.commit()
        self.db.refresh(post)
        return post

    def delete(self, post):
        self.db.delete(post)
        self.db.commit()
Enter fullscreen mode Exit fullscreen mode

The service:

class PostService:
    def __init__(self, repository):
        self.repository = repository

    def get_all_posts(self):
        return self.repository.get_all()
Enter fullscreen mode Exit fullscreen mode

The controller:

@router.get("/posts")
def get_posts():
    return post_service.get_all_posts()
Enter fullscreen mode Exit fullscreen mode

4.4 โœ… Advantages

  • Database logic stays in one place
  • Easier unit testing
  • Reduces duplication
  • Business logic doesn't need to know SQL details
  • Easier to maintain

4.5 ๐Ÿค” Can we change databases with zero changes?

Not automatically! PostgreSQL and MongoDB have different data models and query behavior. The Repository Pattern isolates much of that difference, but changing databases may still require changes to repositories, models, transactions and other persistence code.

๐Ÿ“ A repository is not just a folder containing SQL queries. It represents the data-access operations the rest of the application needs.


5. ๐Ÿญ Factory Pattern

Creational pattern

5.1 ๐Ÿšจ The problem

Your app supports email, SMS and push notifications. You could create objects directly everywhere:

email = EmailNotification()
sms = SMSNotification()
push = PushNotification()
Enter fullscreen mode Exit fullscreen mode

But with 50 places creating notifications, what happens when the creation process changes? You must update many places.

5.2 ๐Ÿค” What is the Factory Pattern?

๐Ÿ“Œ The Factory Pattern provides a separate component or method responsible for creating objects. Instead of creating objects yourself everywhere, you ask a factory to create the appropriate one.

๐Ÿ• Think of ordering pizza or a burger. You don't walk into the kitchen and make it yourself. The kitchen receives your order and creates the item. That's a factory.

5.3 ๐Ÿ Python example

class EmailNotification:
    def send(self, message):
        print("Sending email:", message)


class SMSNotification:
    def send(self, message):
        print("Sending SMS:", message)


class PushNotification:
    def send(self, message):
        print("Sending push notification:", message)
Enter fullscreen mode Exit fullscreen mode

The factory:

class NotificationFactory:
    @staticmethod
    def create_notification(notification_type):
        if notification_type == "email":
            return EmailNotification()
        elif notification_type == "sms":
            return SMSNotification()
        elif notification_type == "push":
            return PushNotification()
        else:
            raise ValueError("Invalid notification type")
Enter fullscreen mode Exit fullscreen mode

Use it:

notification = NotificationFactory.create_notification("email")
notification.send("Your post was liked!")
Enter fullscreen mode Exit fullscreen mode
Sending email: Your post was liked!
Enter fullscreen mode Exit fullscreen mode

5.4 ๐Ÿ” The flow

Application: "I need an email notification."
        โ–ผ
NotificationFactory โ†’ decides which object to create
        โ–ผ
EmailNotification object โ†’ created and returned
Enter fullscreen mode Exit fullscreen mode

5.5 ๐Ÿ“ When to use a Factory

  • Creating different notification types
  • Creating different payment processors
  • Creating different database clients
  • Creating different file parsers
  • Creating different authentication providers

5.6 โš–๏ธ Factory vs normal object creation

Normal creation Factory
Application creates objects directly Factory creates objects
Creation logic may be duplicated Creation logic is centralized
Application knows concrete classes Application can depend on a common interface

๐Ÿง  Factory focuses on HOW objects are created.


6. ๐ŸŽฏ Strategy Pattern

Behavioral pattern. One of the MOST important patterns for backend development.

6.1 ๐Ÿšจ The problem

A payment system supports UPI, credit card, debit card and PayPal. Each processes payments differently. Without the Strategy Pattern:

def make_payment(payment_type, amount):
    if payment_type == "upi":
        print("Processing UPI payment")
    elif payment_type == "card":
        print("Processing card payment")
    elif payment_type == "paypal":
        print("Processing PayPal payment")
Enter fullscreen mode Exit fullscreen mode

Fine at first. But with 20 payment methods the function becomes huge, and every new method means modifying the same function.

6.2 ๐Ÿค” What is the Strategy Pattern?

๐Ÿ“Œ The Strategy Pattern lets you define multiple algorithms, put each in a separate class, and select the appropriate one at runtime.

In simple words: same task, different ways of performing it. ๐ŸšŒ To travel to college you can choose bus, car, bike or metro. The destination is the same, but the method changes.

6.3 ๐Ÿ Implementation

Step 1: create different strategies

class UpiPayment:
    def pay(self, amount):
        print(f"Paid {amount} using UPI")


class CardPayment:
    def pay(self, amount):
        print(f"Paid {amount} using Card")


class PayPalPayment:
    def pay(self, amount):
        print(f"Paid {amount} using PayPal")
Enter fullscreen mode Exit fullscreen mode

Each class implements the same operation, pay(), but performs it differently.

Step 2: create the context class (the class that uses a selected strategy)

class PaymentProcessor:
    def __init__(self, strategy):
        self.strategy = strategy

    def make_payment(self, amount):
        self.strategy.pay(amount)
Enter fullscreen mode Exit fullscreen mode

We pass a payment strategy object into PaymentProcessor. It doesn't need to know whether it's UPI, Card or PayPal. It only needs an object that supports pay().

Step 3: use different strategies

upi = UpiPayment()
processor = PaymentProcessor(upi)
processor.make_payment(500)
Enter fullscreen mode Exit fullscreen mode
Paid 500 using UPI
Enter fullscreen mode Exit fullscreen mode

Change the strategy:

card = CardPayment()
processor = PaymentProcessor(card)
processor.make_payment(1000)
Enter fullscreen mode Exit fullscreen mode
Paid 1000 using Card
Enter fullscreen mode Exit fullscreen mode
PaymentProcessor (Context) โ†’ uses a selected payment strategy
        โ”œโ”€โ”€ UPI     โ†’ pay()
        โ”œโ”€โ”€ Card    โ†’ pay()
        โ””โ”€โ”€ PayPal  โ†’ pay()
Enter fullscreen mode Exit fullscreen mode

6.4 ๐Ÿ“ฑ Strategy in a social-feed project

Different feed-generation algorithms:

  1. Chronological: newest posts first
  2. Popularity-based: most liked posts first
  3. Personalized: posts based on user interests

Instead of one huge function, create separate strategies:

class ChronologicalFeed:
    def generate(self, posts):
        return sorted(
            posts,
            key=lambda post: post.created_at,
            reverse=True,
        )


class PopularityFeed:
    def generate(self, posts):
        return sorted(
            posts,
            key=lambda post: post.like_count,
            reverse=True,
        )
Enter fullscreen mode Exit fullscreen mode

A context uses either one:

class FeedService:
    def __init__(self, strategy):
        self.strategy = strategy

    def get_feed(self, posts):
        return self.strategy.generate(posts)
Enter fullscreen mode Exit fullscreen mode
service = FeedService(ChronologicalFeed())
feed = service.get_feed(posts)
Enter fullscreen mode Exit fullscreen mode

You can change the strategy without rewriting FeedService.

6.5 โœ… Advantages

  • Easy to add new algorithms
  • Avoids huge if-elif conditions
  • Reduces code duplication
  • Makes algorithms independently testable
  • Supports the Open/Closed Principle

6.6 ๐Ÿ“– Important terminology

Term Meaning
Strategy An interchangeable algorithm
Concrete Strategy The actual implementation, such as UPI or Card
Context The class that uses the strategy
Runtime selection Choosing which strategy to use while the app runs

๐ŸŽค Interview answer: The Strategy Pattern defines a family of interchangeable algorithms, encapsulates each in a separate class, and lets the application select one at runtime without changing the client code.


7. ๐ŸŽ Decorator Pattern

Structural pattern. It lets you add extra functionality to existing objects without modifying their original classes.

7.1 ๐Ÿšจ The problem

A coffee-ordering app has a simple coffee. Customers want milk, chocolate, whipped cream, milk and chocolate, and so on. One approach is a class for each combination:

class SimpleCoffee:
    pass


class CoffeeWithMilk:
    pass


class CoffeeWithChocolate:
    pass


class CoffeeWithMilkAndChocolate:
    pass
Enter fullscreen mode Exit fullscreen mode

With 15 toppings you may end up with a huge number of combinations. This is the class explosion problem. ๐Ÿ’ฅ

7.2 ๐Ÿค” What is the Decorator Pattern?

๐Ÿ“Œ The Decorator Pattern dynamically adds responsibilities or functionality to an object by wrapping it inside another object.

Take an existing object, wrap it, and give it extra functionality without changing the original. โ˜• You buy a coffee, then add milk, chocolate and cream. Your original coffee still exists. You're just adding features to it.

7.3 ๐Ÿ Python implementation

Step 1: the basic component

class Coffee:
    def cost(self):
        return 100

    def description(self):
        return "Basic Coffee"
Enter fullscreen mode Exit fullscreen mode

Step 2: a decorator for milk

class MilkDecorator:
    def __init__(self, coffee):
        self.coffee = coffee

    def cost(self):
        return self.coffee.cost() + 20

    def description(self):
        return self.coffee.description() + " + Milk"
Enter fullscreen mode Exit fullscreen mode

Step 3: a decorator for chocolate

class ChocolateDecorator:
    def __init__(self, coffee):
        self.coffee = coffee

    def cost(self):
        return self.coffee.cost() + 30

    def description(self):
        return self.coffee.description() + " + Chocolate"
Enter fullscreen mode Exit fullscreen mode

Step 4: combine them

coffee = Coffee()
coffee = MilkDecorator(coffee)
coffee = ChocolateDecorator(coffee)

print(coffee.description())
print(coffee.cost())
Enter fullscreen mode Exit fullscreen mode
Basic Coffee + Milk + Chocolate
150
Enter fullscreen mode Exit fullscreen mode
Basic Coffee        โ‚น100
   โ–ผ wrapped by
MilkDecorator       +โ‚น20
   โ–ผ wrapped by
ChocolateDecorator  +โ‚น30
   โ–ผ
Final coffee        โ‚น150
Enter fullscreen mode Exit fullscreen mode

coffee = MilkDecorator(coffee) passes the original coffee into MilkDecorator. Then coffee = ChocolateDecorator(coffee) passes the milk-decorated coffee into ChocolateDecorator. This is called wrapping.

7.4 โšก Real-world example: FastAPI decorators

You've probably already used decorators!

@router.get("/posts")
def get_posts():
    return {"message": "Posts"}


@router.post("/posts")
def create_post():
    return {"message": "Created"}
Enter fullscreen mode Exit fullscreen mode

Python decorators are a language feature that can wrap or modify function behavior. FastAPI uses them to register route functions and associate HTTP methods and paths with them. More examples:

@staticmethod
def create():
    pass


@property
def username(self):
    return self._username


@functools.lru_cache
def get_settings():
    return Settings()
Enter fullscreen mode Exit fullscreen mode

โš ๏ธ Important distinction: the Decorator Design Pattern is a particular object-composition pattern. Not every Python @decorator is an implementation of the GoF Decorator Pattern.

7.5 โœ… Advantages

  • Adds functionality dynamically
  • Avoids modifying existing classes
  • Avoids creating many subclasses for combinations
  • Supports the Open/Closed Principle
  • Allows multiple decorators to be combined

7.6 ๐Ÿ“ When to use it

  • Logging
  • Caching
  • Authentication wrappers
  • Authorization checks
  • Adding features to objects
  • Compression and encryption wrappers

8. ๐Ÿ”Œ Adapter Pattern

Structural pattern. One of the easiest to understand with a real-life example.

8.1 ๐Ÿšจ The problem

Your phone has a USB-C port, but your charger has a different connector. Can you connect them directly? No! You need an adapter that converts one interface into another compatible one. Software works the same way.

8.2 ๐Ÿค” What is the Adapter Pattern?

๐Ÿ“Œ The Adapter Pattern lets two incompatible interfaces work together by introducing a middle component that converts one interface into the one expected by the other. Think of it as a translator between two incompatible systems. ๐Ÿ—ฃ๏ธ

8.3 ๐Ÿ Python example

Your application expects every payment gateway to have pay(amount). But an external provider has make_payment(value), so your app can't use it where pay() is expected.

Step 1: the external payment provider

class ExternalPaymentGateway:
    def make_payment(self, value):
        print(f"External payment: {value}")
Enter fullscreen mode Exit fullscreen mode

Step 2: create an adapter

class PaymentAdapter:
    def __init__(self, gateway):
        self.gateway = gateway

    def pay(self, amount):
        self.gateway.make_payment(amount)
Enter fullscreen mode Exit fullscreen mode

Step 3: use the adapter

external_gateway = ExternalPaymentGateway()
adapter = PaymentAdapter(external_gateway)

adapter.pay(500)
Enter fullscreen mode Exit fullscreen mode
External payment: 500
Enter fullscreen mode Exit fullscreen mode
  1. The application calls adapter.pay(500)
  2. The adapter receives the amount
  3. The adapter converts the call into gateway.make_payment(500)
  4. The external gateway processes the payment
Your Application      โ†’ calls pay(amount)
        โ–ผ
Payment Adapter       โ†’ translates the method call
        โ–ผ
External Payment Gateway โ†’ receives make_payment(value)
Enter fullscreen mode Exit fullscreen mode

8.4 ๐ŸŒ Real-world examples

Your app might integrate Razorpay, Stripe, PayPal or different cloud storage providers. Each has different APIs and method names. Create adapters that expose a common interface to your application:

class StripeAdapter:
    def __init__(self, stripe_client):
        self.client = stripe_client

    def charge(self, amount):
        return self.client.create_payment_intent(
            amount=amount,
        )
Enter fullscreen mode Exit fullscreen mode

Your app uses charge() without needing to understand all the provider-specific details.

๐Ÿ“ In production you'd also handle currency units, authentication, errors and payment verification properly.

8.5 โœ… Advantages

  • Integrates third-party services
  • Reuses existing code
  • Avoids modifying external libraries
  • Reduces coupling between your app and external systems
  • Makes switching providers easier

๐ŸŽค Interview answer: The Adapter Pattern converts the interface of an existing class into another interface expected by the client, allowing incompatible classes to work together.


9. ๐Ÿงฌ Composition Over Inheritance

An object-oriented design principle, and VERY important for writing clean code.

9.1 ๐Ÿ‘ช What is inheritance?

Inheritance lets one class acquire properties and methods from another:

class Animal:
    def eat(self):
        print("Animal is eating")


class Dog(Animal):
    def bark(self):
        print("Dog is barking")
Enter fullscreen mode Exit fullscreen mode
dog = Dog()
dog.eat()
dog.bark()
Enter fullscreen mode Exit fullscreen mode
Animal is eating
Dog is barking
Enter fullscreen mode Exit fullscreen mode

Dog inherits eat() from Animal. This is an IS-A relationship: a dog is an animal.

9.2 ๐Ÿงฉ What is composition?

๐Ÿ“Œ Composition means creating a class using objects of other classes. Instead of inheriting functionality, a class contains or uses another object to perform a task. This is a HAS-A relationship.

A car has an engine. A car is not an engine. ๐Ÿš—

class Engine:
    def start(self):
        print("Engine started")


class Car:
    def __init__(self):
        self.engine = Engine()

    def start_car(self):
        self.engine.start()
        print("Car is ready")
Enter fullscreen mode Exit fullscreen mode
car = Car()
car.start_car()
Enter fullscreen mode Exit fullscreen mode
Engine started
Car is ready
Enter fullscreen mode Exit fullscreen mode

self.engine = Engine() means the Car contains an Engine object and uses its functionality instead of inheriting from it.

9.3 โš–๏ธ Inheritance vs composition

Inheritance:  Animal โ—€โ”€โ”€ inherits from โ”€โ”€ Dog     (IS-A)
Composition:  Car โ”€โ”€ HAS-A โ”€โ”€โ–ถ Engine             (uses another object)
Enter fullscreen mode Exit fullscreen mode

9.4 ๐Ÿค” Why is composition often preferred?

A notification system has:

class EmailNotification:
    pass


class SMSNotification:
    pass


class PushNotification:
    pass
Enter fullscreen mode Exit fullscreen mode

You want logging on every notification. One approach is inheritance:

class EmailWithLogging(EmailNotification):
    pass


class SMSWithLogging(SMSNotification):
    pass


class PushWithLogging(PushNotification):
    pass
Enter fullscreen mode Exit fullscreen mode

Now add logging, caching, retry and monitoring, and you may create a huge number of subclasses. That's the inheritance explosion problem. Use composition instead:

class Logger:
    def log(self, message):
        print("LOG:", message)


class NotificationService:
    def __init__(self, logger):
        self.logger = logger

    def send(self, message):
        self.logger.log("Sending notification")
        print("Notification sent:", message)
Enter fullscreen mode Exit fullscreen mode
logger = Logger()
service = NotificationService(logger)
service.send("Hello!")
Enter fullscreen mode Exit fullscreen mode

Now you can replace the Logger with another implementation without changing the notification service.

9.5 โœ… Advantages of composition

  • More flexible code
  • Less dependency on parent classes
  • Easier testing
  • Easier to replace components
  • Supports loose coupling
  • Encourages reusable functionality

9.6 ๐Ÿคท Does composition mean never use inheritance?

Absolutely not! Inheritance is useful when a genuine IS-A relationship exists: a Dog is an Animal, an AdminUser is a User, a Circle is a Shape. Composition is preferable when you want to reuse behavior or collaborate with other objects without creating an artificial parent-child relationship.

๐ŸŽค "Favor composition over inheritance" means prefer combining objects with specific responsibilities over building deep, rigid inheritance hierarchies.


10. ๐Ÿงต How All These Patterns Work Together

A user uploads a post containing an image. A well-organized architecture could work like this:

โš›๏ธ React Frontend โ†’ user creates a post with an image
        โ–ผ
๐Ÿ›ฃ๏ธ Controller / FastAPI Router โ†’ receives and validates the HTTP request
        โ–ผ
๐Ÿง  PostService โ†’ applies post creation rules
        โ”‚
        โ”œโ”€โ”€ ๐Ÿญ Factory   โ†’ creates the appropriate storage provider
        โ”œโ”€โ”€ ๐Ÿ”Œ Adapter   โ†’ converts the storage provider's API into your app's expected interface
        โ”œโ”€โ”€ ๐ŸŽฏ Strategy  โ†’ selects an image-processing or compression algorithm
        โ””โ”€โ”€ ๐ŸŽ Decorator โ†’ adds optional logging, caching or monitoring around an operation
        โ–ผ
๐Ÿ“š PostRepository โ†’ saves post information in PostgreSQL
        โ–ผ
๐Ÿ˜ PostgreSQL โ†’ stores post records and image URLs
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ These are possible applications of the patterns, not a requirement that every service must use all five.

The main lesson: each component has a clear responsibility.


11. โš–๏ธ Separation of Concerns vs Loose Coupling vs High Cohesion

These three are often discussed together.

11.1 ๐Ÿงน Separation of Concerns

Each component has a specific responsibility. The controller handles HTTP requests, the service handles business logic, the repository handles database access. Don't mix everything into one class.

11.2 ๐Ÿ”— Loose Coupling

Coupling is how strongly two components depend on each other.

  • Tight coupling: one component heavily depends on the internal implementation of another
  • Loose coupling: components interact through well-defined interfaces and have fewer dependencies on each other's internals

โŒ Tightly coupled:

class NotificationService:
    def send(self, message):
        email = EmailNotification()
        email.send(message)
Enter fullscreen mode Exit fullscreen mode

Here NotificationService directly creates an EmailNotification, so it's permanently dependent on that concrete class.

โœ… Loosely coupled:

class NotificationService:
    def __init__(self, notification):
        self.notification = notification

    def send(self, message):
        self.notification.send(message)
Enter fullscreen mode Exit fullscreen mode

Now you can inject EmailNotification, SMSNotification or PushNotification. The service doesn't need to know which concrete implementation it receives. This is Dependency Injection. ๐Ÿ’‰

11.3 ๐ŸŽฏ High Cohesion

Cohesion is how closely related the responsibilities inside a component are.

  • High cohesion: a class or module focuses on one closely related set of responsibilities
  • Low cohesion: a class handles unrelated responsibilities

โŒ Bad design:

class UserManager:
    def create_user(self):
        pass

    def send_email(self):
        pass

    def generate_report(self):
        pass

    def process_payment(self):
        pass
Enter fullscreen mode Exit fullscreen mode

โœ… Better design:

class UserService:
    def create_user(self):
        pass


class EmailService:
    def send_email(self):
        pass


class ReportService:
    def generate_report(self):
        pass


class PaymentService:
    def process_payment(self):
        pass
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“Š Comparison

Concept Meaning Main goal
Separation of Concerns Divide responsibilities Organized architecture
Loose Coupling Reduce dependencies Flexibility
High Cohesion Keep related responsibilities together Focused components

๐Ÿง  Good software design = separation of concerns + loose coupling + high cohesion + clear responsibilities + easy testing and maintenance.


12. ๐ŸฅŠ Important Differences Between the Patterns

๐ŸŽฏ This is where interviewers may try to confuse you.

12.1 ๐Ÿญ Factory vs ๐ŸŽฏ Strategy

Both use different classes, but they solve different problems.

Factory Strategy
Creates objects Selects behavior
Focuses on object creation Focuses on algorithms
Which object should I create? Which algorithm should I use?
Example: create a UPI or Card processor Example: use a UPI or Card payment strategy

A factory might create a UPI payment object, and the strategy determines how it processes a payment. They can also be used together.

12.2 ๐Ÿ”Œ Adapter vs ๐ŸŽ Decorator

Both can wrap another object, but their purposes differ.

Adapter Decorator
Changes the interface Adds functionality
Makes incompatible objects compatible Enhances existing object behavior
Acts as a translator Acts as a wrapper
Example: converts make_payment() into pay() Example: adds logging to pay()

12.3 ๐Ÿ“š Repository vs ๐Ÿง  Service

Repository Service
Handles database operations Handles business rules
Saves, retrieves, deletes data Decides what should happen
Knows persistence details Should avoid persistence implementation details
Example: get_post_by_id() Example: create_post()

12.4 ๐Ÿงฌ Inheritance vs Composition vs Decorator

Concept Main idea
Inheritance Get functionality from a parent class
Composition Use another object as a component
Decorator Wrap an object to add behavior

13. ๐Ÿงญ How to Identify Which Pattern to Use

Your problem Suitable pattern
Database queries are scattered across controllers ๐Ÿ“š Repository
Need to create different notification objects ๐Ÿญ Factory
Need multiple feed-generation algorithms ๐ŸŽฏ Strategy
Need to add logging without modifying original classes ๐ŸŽ Decorator
A third-party API has an incompatible method ๐Ÿ”Œ Adapter
One class inherits too many unrelated behaviors ๐Ÿงฌ Composition
One class is doing everything ๐Ÿงน Separation of Concerns / SRP

14. โš ๏ธ Common Mistakes Beginners Make

  1. โŒ Using design patterns everywhere. Not every class needs a Factory, Adapter or Strategy. Use patterns when they solve an actual problem.
  2. โŒ Putting business logic inside repositories. Repositories should primarily handle data access.
  3. โŒ Putting SQL queries inside controllers. Delegate persistence operations to the repository layer.
  4. โŒ Confusing Factory and Strategy. Factory creates objects; Strategy selects interchangeable behavior.
  5. โŒ Confusing Adapter and Decorator. Adapter changes compatibility; Decorator adds functionality.
  6. โŒ Creating unnecessarily deep inheritance hierarchies. Prefer composition when objects need to collaborate rather than inherit from each other.
  7. โŒ Creating huge service classes. Keep related business responsibilities together.

15. ๐ŸŽค Interview Questions and Answers

Q1. What are design patterns?
Reusable approaches to commonly occurring software design problems. They help developers write maintainable, flexible and reusable code.

Q2. What is layered architecture?
It divides an application into separate layers such as Controller, Service and Repository, where each layer has a specific responsibility.

Q3. Why do we use the Repository Pattern?
To separate database access logic from business logic, improving maintainability, reusability and testability.

Q4. What is the Factory Pattern?
A centralized way to create objects without exposing all object-creation details to the client.

Q5. What is the Strategy Pattern?
It encapsulates different algorithms into separate interchangeable classes and lets the application select one at runtime.

Q6. What is the Decorator Pattern?
It dynamically adds behavior to an existing object without modifying its original class.

Q7. What is the Adapter Pattern?
It allows incompatible interfaces to work together by converting one interface into another expected interface.

Q8. What is composition over inheritance?
The principle of preferring object composition and collaboration over unnecessary inheritance hierarchies.

Q9. What is dependency injection?
A technique in which an object receives its dependencies from outside rather than creating them internally.

Q10. What are the benefits of separation of concerns?
It improves readability, maintainability, testability and reusability, and makes individual components easier to modify.


16. ๐Ÿ“ Final Revision Sheet

Pattern / principle Category Example Idea
๐Ÿ“š Repository Structural / data access PostRepository Separates database operations from business logic
๐Ÿญ Factory Creational NotificationFactory Creates the appropriate object
๐ŸŽฏ Strategy Behavioral Payment strategies Selects between interchangeable algorithms
๐ŸŽ Decorator Structural Logging decorator Adds functionality by wrapping an object
๐Ÿ”Œ Adapter Structural Payment API adapter Makes incompatible interfaces work together
๐Ÿงฌ Composition Design principle Car HAS an Engine Builds functionality from collaborating objects
๐Ÿฅž Layered Architecture Architectural pattern Controller โ†’ Service โ†’ Repository Separates responsibilities into layers
๐Ÿงน Separation of Concerns Design principle One focused responsibility per component Keeps different responsibilities separate

17. ๐Ÿงช Test Your Understanding!

Try these before peeking at the answers! โค๏ธ

  1. Your app supports UPI, PayPal and Card payments, and you want to choose a payment algorithm at runtime. Which pattern?

    • A) Factory
    • B) Strategy
    • C) Adapter
    • D) Repository
  2. Your controller contains SQL queries. You want to move all database operations into a separate component. Which pattern?

    • A) Decorator
    • B) Factory
    • C) Repository
    • D) Adapter
  3. An external API uses make_payment(), but your app expects pay(). What do you use?

    • A) Adapter
    • B) Strategy
    • C) Decorator
    • D) Factory
  4. You want to add logging to an existing object without modifying its class. Which pattern?

    • A) Repository
    • B) Factory
    • C) Decorator
    • D) Strategy
  5. A Car uses an Engine object instead of inheriting from Engine. What is this?

    • A) Inheritance
    • B) Composition
    • C) Adapter
    • D) Factory
  6. You want one class to create EmailNotification, SMSNotification or PushNotification objects. Which pattern?

    • A) Strategy
    • B) Factory
    • C) Repository
    • D) Decorator

๐Ÿ‘€ Click to reveal answers
  1. B: Strategy. Interchangeable algorithms chosen at runtime
  2. C: Repository. It separates data access from the controller
  3. A: Adapter. It translates one interface into another
  4. C: Decorator. It wraps an object to add behavior
  5. B: Composition. This is a HAS-A relationship
  6. B: Factory. It centralizes object creation


18. ๐Ÿ What to Remember

  1. ๐Ÿšซ Don't put everything in one file
  2. ๐Ÿ›ฃ๏ธ Keep controllers responsible for HTTP communication
  3. ๐Ÿง  Keep services responsible for business rules
  4. ๐Ÿ“š Keep repositories responsible for database operations
  5. ๐Ÿญ Use Factory when object creation becomes complex
  6. ๐ŸŽฏ Use Strategy when multiple algorithms are interchangeable
  7. ๐ŸŽ Use Decorator when you need to add behavior without modifying the original object
  8. ๐Ÿ”Œ Use Adapter when integrating incompatible external systems
  9. ๐Ÿงฌ Prefer composition when objects need to collaborate
  10. ๐Ÿ”— Aim for loose coupling and high cohesion

๐ŸŽฏ The ultimate goal isn't to use as many design patterns as possible. It's to write software that is easy to understand, extend, test and maintain.


If this post helped you, drop a โค๏ธ or ๐Ÿฆ„ and share it with a friend learning software design. Happy coding! ๐Ÿ‘ฉโ€๐Ÿ’ปโœจ

Top comments (0)