🧩 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(Source:https://dev.to/deepika_pusala/structural-behavioral-design-patterns-in-python-repository-factory-strategy-decorator--3lldRelated posts
![Industrial Communication — Ch.10: Factory Automation [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft1hw2kvh64lkchmsurzd.png)
Industrial Communication — Ch.10: Factory Automation [EN]

Your product depends on one AI provider: when it cuts you off, what does your code do?
FreeBSD Capsicum requires a file-descriptor rights design
I got tired of waiting for APIs to be ready, so I built a mock server that runs on my laptop
