Overview
Choosing the right architecture pattern is crucial for building scalable, maintainable systems. Each pattern has trade-offs, and the best choice depends on your specific context: team size, domain complexity, scale requirements, and organizational structure.Conway’s Law: “Organizations design systems that mirror their own communication structure.” Your architecture should align with your team structure.
Monolithic Architecture
Copy
┌─────────────────────────────────────────────────┐
│ Monolith │
│ ┌──────────┬──────────┬──────────┬──────────┐ │
│ │ UI │ Users │ Orders │ Payments │ │
│ └──────────┴──────────┴──────────┴──────────┘ │
│ ┌─────────────────────────────────────────────┐│
│ │ Shared Database ││
│ └─────────────────────────────────────────────┘│
└─────────────────────────────────────────────────┘
✅ Pros
- Simple to develop & deploy
- Easy debugging (single process)
- No network latency between modules
- ACID transactions easy
❌ Cons
- Hard to scale individual parts
- Long deployment cycles
- Technology lock-in
- One bug can crash everything
Microservices Architecture
Copy
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ User │ │ Order │ │ Payment │ │ Notif │
│ Service │ │ Service │ │ Service │ │ Service │
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
│ │ │ │
└────────────┴─────┬──────┴────────────┘
│
┌─────────┴─────────┐
│ API Gateway │
└───────────────────┘
│
┌─────────┴─────────┐
│ Clients │
└───────────────────┘
✅ Pros
- Independent scaling
- Technology diversity
- Fault isolation
- Faster deployments
❌ Cons
- Distributed system complexity
- Network latency
- Data consistency challenges
- Operational overhead
Service Communication
Copy
# Synchronous (HTTP/REST)
import requests
def get_user_orders(user_id):
user = requests.get(f"http://user-service/users/{user_id}").json()
orders = requests.get(f"http://order-service/orders?user={user_id}").json()
return {"user": user, "orders": orders}
# Asynchronous (Message Queue)
import pika
def publish_order_created(order):
channel.basic_publish(
exchange='orders',
routing_key='order.created',
body=json.dumps(order)
)
Event-Driven Architecture
Copy
┌──────────────────────────────────────────────────────┐
│ Event Bus │
└────┬─────────┬─────────┬─────────┬─────────┬────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Order │ │Inventory│ │ Payment│ │ Email │ │Analytics│
│ Service│ │ Service │ │ Service│ │ Service│ │ Service│
└────────┘ └─────────┘ └────────┘ └────────┘ └─────────┘
Event Sourcing
Copy
# Store events, not state
class OrderEventStore:
def append(self, event):
self.events.append({
"type": event.type,
"data": event.data,
"timestamp": datetime.now()
})
def get_order_state(self, order_id):
# Replay events to get current state
order = Order()
for event in self.events:
if event["data"]["order_id"] == order_id:
order.apply(event)
return order
# Event types
OrderCreated(order_id="123", items=[...])
PaymentReceived(order_id="123", amount=100)
OrderShipped(order_id="123", tracking="XYZ")
CQRS (Command Query Responsibility Segregation)
Copy
Commands (Write) Queries (Read)
│ │
▼ ▼
┌─────────┐ ┌─────────────┐
│ Command │ │ Query │
│ Handler │ │ Handler │
└────┬────┘ └──────┬──────┘
│ │
▼ ▼
┌─────────┐ Sync/ ┌───────────┐
│ Write │ ─────────────► │ Read │
│ DB │ Events │ DB │
└─────────┘ └───────────┘
Layered Architecture
Copy
┌─────────────────────────────────────┐
│ Presentation Layer │ ◄─── UI, Controllers
├─────────────────────────────────────┤
│ Application Layer │ ◄─── Use Cases, Services
├─────────────────────────────────────┤
│ Domain Layer │ ◄─── Business Logic, Entities
├─────────────────────────────────────┤
│ Infrastructure Layer │ ◄─── DB, External APIs
└─────────────────────────────────────┘
Copy
# Clean Architecture Example
# Domain Layer (innermost - no dependencies)
class Order:
def __init__(self, id, items):
self.id = id
self.items = items
self.status = "pending"
def calculate_total(self):
return sum(item.price for item in self.items)
# Application Layer (use cases)
class CreateOrderUseCase:
def __init__(self, order_repo, payment_service):
self.order_repo = order_repo
self.payment_service = payment_service
def execute(self, order_data):
order = Order(**order_data)
self.payment_service.process(order.calculate_total())
self.order_repo.save(order)
return order
# Infrastructure Layer (outermost)
class PostgresOrderRepository:
def save(self, order):
# SQL implementation
pass
class StripePaymentService:
def process(self, amount):
# Stripe API call
pass
Comparison Matrix
| Pattern | Complexity | Scalability | Team Size | Use Case |
|---|---|---|---|---|
| Monolith | 🟢 Low | 🟡 Medium | Small | MVPs, Startups |
| Microservices | 🔴 High | 🟢 High | Large | Complex domains |
| Event-Driven | 🔴 High | 🟢 High | Medium+ | Real-time, Async |
| Layered | 🟢 Low | 🟡 Medium | Any | Traditional apps |
Hexagonal Architecture (Ports and Adapters)
The core business logic is isolated from external concerns through ports (interfaces) and adapters (implementations).Copy
┌─────────────────────────────────────┐
│ │
┌───────────┐ │ ┌─────────────────────────────┐ │ ┌───────────┐
│ REST API │───┼──►│ PORT │ │ │ Database │
│ Adapter │ │ │ (Input Interface) │ │ │ Adapter │
└───────────┘ │ └───────────┬─────────────────┘ │ └───────────┘
│ │ │ ▲
┌───────────┐ │ ▼ │ │
│ CLI │───┼──►┌─────────────────────────────┐ │ ┌─────┴─────┐
│ Adapter │ │ │ │ │ │ PORT │
└───────────┘ │ │ DOMAIN / CORE │◄──┼───│ (Output) │
│ │ (Business Logic) │ │ └───────────┘
┌───────────┐ │ │ │ │ │
│ gRPC │───┼──►└─────────────────────────────┘ │ ▼
│ Adapter │ │ │ │ ┌───────────┐
└───────────┘ │ ▼ │ │ Email │
│ ┌─────────────────────────────┐ │ │ Adapter │
│ │ PORT │───┼──►└───────────┘
│ │ (Output Interface) │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────┘
Copy
# Port (Interface)
from abc import ABC, abstractmethod
class UserRepository(ABC):
@abstractmethod
def save(self, user: User) -> None: pass
@abstractmethod
def find_by_id(self, user_id: str) -> User: pass
# Adapter (Implementation)
class PostgresUserRepository(UserRepository):
def __init__(self, connection):
self.connection = connection
def save(self, user: User) -> None:
self.connection.execute(
"INSERT INTO users ...", user.to_dict()
)
def find_by_id(self, user_id: str) -> User:
row = self.connection.query("SELECT * FROM users WHERE id = ?", user_id)
return User.from_dict(row)
# Domain service (core business logic, no external dependencies)
class UserService:
def __init__(self, user_repo: UserRepository): # Inject port
self.user_repo = user_repo
def register_user(self, name: str, email: str) -> User:
user = User(name=name, email=email)
user.validate() # Business rule
self.user_repo.save(user)
return user
Domain-Driven Design (DDD) Concepts
Strategic Design
Copy
┌─────────────────────────────────────────────────────────────────┐
│ E-Commerce System │
├─────────────────┬─────────────────┬─────────────────────────────┤
│ Order Context │ Catalog Context│ Shipping Context │
│ │ │ │
│ - Order │ - Product │ - Shipment │
│ - OrderItem │ - Category │ - Carrier │
│ - Customer* │ - Inventory │ - TrackingInfo │
│ │ │ - Customer* │
│ │ │ │
│ * Different │ │ * Different view of │
│ view of │ │ Customer than in │
│ Customer │ │ Order Context │
└─────────────────┴─────────────────┴─────────────────────────────┘
│ │ │
└───────────────────┼──────────────────────┘
│
Anti-Corruption Layer
(Context Mapping)
Tactical Design Patterns
Copy
# Entity (has identity)
class Order:
def __init__(self, order_id: str):
self.id = order_id # Identity
self.items = []
self.status = "pending"
def add_item(self, product_id: str, quantity: int, price: Decimal):
self.items.append(OrderItem(product_id, quantity, price))
def calculate_total(self) -> Decimal:
return sum(item.subtotal for item in self.items)
# Value Object (no identity, immutable)
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str
def add(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError("Currency mismatch")
return Money(self.amount + other.amount, self.currency)
# Aggregate (cluster of entities with a root)
class Order: # Aggregate Root
def __init__(self, order_id: str, customer_id: str):
self.id = order_id
self.customer_id = customer_id
self._items: List[OrderItem] = [] # Part of aggregate
def add_item(self, product: Product, quantity: int):
# Business rule: max 10 items per order
if len(self._items) >= 10:
raise DomainError("Order cannot have more than 10 items")
self._items.append(OrderItem(product.id, quantity, product.price))
# Domain Event
@dataclass
class OrderPlaced:
order_id: str
customer_id: str
total: Decimal
timestamp: datetime
# Repository (persistence abstraction)
class OrderRepository(ABC):
@abstractmethod
def save(self, order: Order) -> None: pass
@abstractmethod
def find_by_id(self, order_id: str) -> Optional[Order]: pass
Saga Pattern (Distributed Transactions)
For maintaining data consistency across microservices without distributed transactions.Choreography-Based Saga
Copy
Order Service Payment Service Inventory Service
│ │ │
│ OrderCreated │ │
├─────────────────────►│ │
│ │ PaymentProcessed │
│ ├─────────────────────►│
│ │ │
│ │ InventoryReserved │
│◄─────────────────────┼──────────────────────┤
│ │ │
│ OrderConfirmed │ │
│ │ │
Compensation (if Inventory fails):
│ │ InventoryFailed │
│◄─────────────────────┼──────────────────────┤
│ RefundPayment │ │
├─────────────────────►│ │
│ OrderCancelled │ │
Orchestration-Based Saga
Copy
class OrderSagaOrchestrator:
def __init__(self, payment_service, inventory_service, notification_service):
self.payment = payment_service
self.inventory = inventory_service
self.notification = notification_service
def execute(self, order: Order):
try:
# Step 1: Process payment
payment_id = self.payment.process(order.total)
# Step 2: Reserve inventory
try:
self.inventory.reserve(order.items)
except InventoryError:
# Compensate: Refund payment
self.payment.refund(payment_id)
raise
# Step 3: Send confirmation
self.notification.send_confirmation(order)
return SagaResult.success(order.id)
except Exception as e:
return SagaResult.failure(str(e))
API Gateway Pattern
Copy
┌─────────────────┐
│ User Service │
└────────▲────────┘
│
┌──────────┐ ┌─────────────────┐ │ ┌─────────────────┐
│ Mobile │─────►│ │───────┼──────►│ Order Service │
│ App │ │ API Gateway │ │ └─────────────────┘
└──────────┘ │ │ │
│ • Auth │ │ ┌─────────────────┐
┌──────────┐ │ • Rate Limit │───────┼──────►│ Payment Service │
│ Web │─────►│ • Routing │ │ └─────────────────┘
│ App │ │ • Aggregation │ │
└──────────┘ │ • Caching │ │ ┌─────────────────┐
│ • SSL Term │───────┴──────►│ Catalog Service│
┌──────────┐ │ • Logging │ └─────────────────┘
│ Partner │─────►│ │
│ API │ └─────────────────┘
└──────────┘
Backend for Frontend (BFF)
Copy
┌──────────┐ ┌─────────────────┐
│ Mobile │─────►│ Mobile BFF │────┐
│ App │ └─────────────────┘ │
└──────────┘ │ ┌─────────────────┐
├───►│ Services │
┌──────────┐ ┌─────────────────┐ │ └─────────────────┘
│ Web │─────►│ Web BFF │────┤
│ App │ └─────────────────┘ │
└──────────┘ │
│
┌──────────┐ ┌─────────────────┐ │
│ Admin │─────►│ Admin BFF │────┘
│ Panel │ └─────────────────┘
└──────────┘
Circuit Breaker Pattern
Prevent cascading failures in distributed systems.Copy
import time
from enum import Enum
class CircuitState(Enum):
CLOSED = "closed" # Normal operation
OPEN = "open" # Failing, reject requests
HALF_OPEN = "half_open" # Testing if service recovered
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=30):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.failure_count = 0
self.last_failure_time = None
self.state = CircuitState.CLOSED
def call(self, func, *args, **kwargs):
if self.state == CircuitState.OPEN:
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
else:
raise CircuitOpenError("Circuit is open")
try:
result = func(*args, **kwargs)
self._on_success()
return result
except Exception as e:
self._on_failure()
raise
def _on_success(self):
self.failure_count = 0
self.state = CircuitState.CLOSED
def _on_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
# Usage
circuit = CircuitBreaker(failure_threshold=3, recovery_timeout=60)
try:
result = circuit.call(external_service.fetch_data)
except CircuitOpenError:
result = cached_data # Fallback
Service Mesh
Copy
┌─────────────────────────────────────────────────────────────────┐
│ Service Mesh │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Service A │ │ Service B │ │
│ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │
│ │ │ App Code │ │ │ │ App Code │ │ │
│ │ └──────┬──────┘ │ │ └──────┬──────┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌──────┴──────┐ │ │ ┌──────┴──────┐ │ │
│ │ │ Sidecar │◄───┼──────┼─►│ Sidecar │ │ │
│ │ │ Proxy │ │ │ │ Proxy │ │ │
│ │ │ (Envoy) │ │ │ │ (Envoy) │ │ │
│ │ └─────────────┘ │ │ └─────────────┘ │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │ │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ │ Control Plane │ │
│ │ (Istio, Linkerd) │ │
│ └─────────────────────┘ │
│ │
│ Features: mTLS, Load Balancing, Circuit Breaking, │
│ Observability, Traffic Management │
└─────────────────────────────────────────────────────────────────┘
Architecture Decision Records (ADR)
Document important architectural decisions.Copy
# ADR-001: Use PostgreSQL for Primary Database
## Status
Accepted
## Context
We need a database for our e-commerce platform that handles:
- Complex queries with JOINs
- ACID transactions for orders
- ~1M daily transactions
## Decision
Use PostgreSQL as our primary database.
## Consequences
### Positive
- Strong ACID compliance
- Rich query capabilities
- Mature ecosystem
### Negative
- Horizontal scaling is complex
- May need read replicas for scale
### Neutral
- Team has moderate PostgreSQL experience
Comparison Matrix
| Pattern | Complexity | Scalability | Team Size | Use Case |
|---|---|---|---|---|
| Monolith | 🟢 Low | 🟡 Medium | Small | MVPs, Startups |
| Modular Monolith | 🟡 Medium | 🟡 Medium | Small-Medium | Growing systems |
| Microservices | 🔴 High | 🟢 High | Large | Complex domains |
| Event-Driven | 🔴 High | 🟢 High | Medium+ | Real-time, Async |
| Serverless | 🟡 Medium | 🟢 High | Small-Medium | Sporadic workloads |
| Layered | 🟢 Low | 🟡 Medium | Any | Traditional apps |
| Hexagonal | 🟡 Medium | 🟡 Medium | Medium | Testable, maintainable |
Interview Tip: Always discuss trade-offs. There’s no “best” architecture—only the right one for your context. Consider team size, domain complexity, scale requirements, and time-to-market.
Common Mistake: Don’t start with microservices. Start with a well-structured monolith, then extract services as needed. Premature decomposition causes more problems than it solves.