๐งฉ 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
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
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)
- React sends a POST request
- FastAPI receives it
- The controller gets the request data
- It calls the service
- 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
-
PostServicehandles 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
- Create a
Postdatabase object - Add it to the database session
- Commit the transaction
- Refresh the object to retrieve updated database values
- 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
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
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
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()
The service:
class PostService:
def __init__(self, repository):
self.repository = repository
def get_all_posts(self):
return self.repository.get_all()
The controller:
@router.get("/posts")
def get_posts():
return post_service.get_all_posts()
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()
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)
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")
Use it:
notification = NotificationFactory.create_notification("email")
notification.send("Your post was liked!")
Sending email: Your post was liked!
5.4 ๐ The flow
Application: "I need an email notification."
โผ
NotificationFactory โ decides which object to create
โผ
EmailNotification object โ created and returned
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")
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")
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)
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)
Paid 500 using UPI
Change the strategy:
card = CardPayment()
processor = PaymentProcessor(card)
processor.make_payment(1000)
Paid 1000 using Card
PaymentProcessor (Context) โ uses a selected payment strategy
โโโ UPI โ pay()
โโโ Card โ pay()
โโโ PayPal โ pay()
6.4 ๐ฑ Strategy in a social-feed project
Different feed-generation algorithms:
- Chronological: newest posts first
- Popularity-based: most liked posts first
- 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,
)
A context uses either one:
class FeedService:
def __init__(self, strategy):
self.strategy = strategy
def get_feed(self, posts):
return self.strategy.generate(posts)
service = FeedService(ChronologicalFeed())
feed = service.get_feed(posts)
You can change the strategy without rewriting FeedService.
6.5 โ Advantages
- Easy to add new algorithms
- Avoids huge
if-elifconditions - 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
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"
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"
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"
Step 4: combine them
coffee = Coffee()
coffee = MilkDecorator(coffee)
coffee = ChocolateDecorator(coffee)
print(coffee.description())
print(coffee.cost())
Basic Coffee + Milk + Chocolate
150
Basic Coffee โน100
โผ wrapped by
MilkDecorator +โน20
โผ wrapped by
ChocolateDecorator +โน30
โผ
Final coffee โน150
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"}
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()
โ ๏ธ Important distinction: the Decorator Design Pattern is a particular object-composition pattern. Not every Python
@decoratoris 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}")
Step 2: create an adapter
class PaymentAdapter:
def __init__(self, gateway):
self.gateway = gateway
def pay(self, amount):
self.gateway.make_payment(amount)
Step 3: use the adapter
external_gateway = ExternalPaymentGateway()
adapter = PaymentAdapter(external_gateway)
adapter.pay(500)
External payment: 500
- The application calls
adapter.pay(500) - The adapter receives the amount
- The adapter converts the call into
gateway.make_payment(500) - The external gateway processes the payment
Your Application โ calls pay(amount)
โผ
Payment Adapter โ translates the method call
โผ
External Payment Gateway โ receives make_payment(value)
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,
)
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")
dog = Dog()
dog.eat()
dog.bark()
Animal is eating
Dog is barking
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")
car = Car()
car.start_car()
Engine started
Car is ready
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)
9.4 ๐ค Why is composition often preferred?
A notification system has:
class EmailNotification:
pass
class SMSNotification:
pass
class PushNotification:
pass
You want logging on every notification. One approach is inheritance:
class EmailWithLogging(EmailNotification):
pass
class SMSWithLogging(SMSNotification):
pass
class PushWithLogging(PushNotification):
pass
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)
logger = Logger()
service = NotificationService(logger)
service.send("Hello!")
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
๐ 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)
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)
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
โ
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
๐ 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
- โ Using design patterns everywhere. Not every class needs a Factory, Adapter or Strategy. Use patterns when they solve an actual problem.
- โ Putting business logic inside repositories. Repositories should primarily handle data access.
- โ Putting SQL queries inside controllers. Delegate persistence operations to the repository layer.
- โ Confusing Factory and Strategy. Factory creates objects; Strategy selects interchangeable behavior.
- โ Confusing Adapter and Decorator. Adapter changes compatibility; Decorator adds functionality.
- โ Creating unnecessarily deep inheritance hierarchies. Prefer composition when objects need to collaborate rather than inherit from each other.
- โ 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! โค๏ธ
-
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
-
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
-
An external API uses
make_payment(), but your app expectspay(). What do you use?- A) Adapter
- B) Strategy
- C) Decorator
- D) Factory
-
You want to add logging to an existing object without modifying its class. Which pattern?
- A) Repository
- B) Factory
- C) Decorator
- D) Strategy
-
A
Caruses anEngineobject instead of inheriting fromEngine. What is this?- A) Inheritance
- B) Composition
- C) Adapter
- D) Factory
-
You want one class to create
EmailNotification,SMSNotificationorPushNotificationobjects. Which pattern?- A) Strategy
- B) Factory
- C) Repository
- D) Decorator
๐ Click to reveal answers
18. ๐ What to Remember
- ๐ซ Don't put everything in one file
- ๐ฃ๏ธ Keep controllers responsible for HTTP communication
- ๐ง Keep services responsible for business rules
- ๐ Keep repositories responsible for database operations
- ๐ญ Use Factory when object creation becomes complex
- ๐ฏ Use Strategy when multiple algorithms are interchangeable
- ๐ Use Decorator when you need to add behavior without modifying the original object
- ๐ Use Adapter when integrating incompatible external systems
- ๐งฌ Prefer composition when objects need to collaborate
- ๐ 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)