Skip to main content

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

┌─────────────────────────────────────────────────┐
│                  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
Best For: Startups, MVPs, small teams, simple domains

Microservices Architecture

┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│  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
Best For: Large teams, complex domains, high scale requirements

Service Communication

# 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

┌──────────────────────────────────────────────────────┐
│                    Event Bus                         │
└────┬─────────┬─────────┬─────────┬─────────┬────────┘
     │         │         │         │         │
     ▼         ▼         ▼         ▼         ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Order  │ │Inventory│ │ Payment│ │ Email  │ │Analytics│
│ Service│ │ Service │ │ Service│ │ Service│ │ Service│
└────────┘ └─────────┘ └────────┘ └────────┘ └─────────┘

Event Sourcing

# 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)

Commands (Write)              Queries (Read)
     │                              │
     ▼                              ▼
┌─────────┐                  ┌─────────────┐
│ Command │                  │    Query    │
│ Handler │                  │   Handler   │
└────┬────┘                  └──────┬──────┘
     │                              │
     ▼                              ▼
┌─────────┐    Sync/         ┌───────────┐
│  Write  │ ─────────────►   │   Read    │
│   DB    │   Events         │   DB      │
└─────────┘                  └───────────┘

Layered Architecture

┌─────────────────────────────────────┐
│        Presentation Layer           │  ◄─── UI, Controllers
├─────────────────────────────────────┤
│         Application Layer           │  ◄─── Use Cases, Services
├─────────────────────────────────────┤
│          Domain Layer               │  ◄─── Business Logic, Entities
├─────────────────────────────────────┤
│        Infrastructure Layer         │  ◄─── DB, External APIs
└─────────────────────────────────────┘
# 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

PatternComplexityScalabilityTeam SizeUse Case
Monolith🟢 Low🟡 MediumSmallMVPs, Startups
Microservices🔴 High🟢 HighLargeComplex domains
Event-Driven🔴 High🟢 HighMedium+Real-time, Async
Layered🟢 Low🟡 MediumAnyTraditional apps

Hexagonal Architecture (Ports and Adapters)

The core business logic is isolated from external concerns through ports (interfaces) and adapters (implementations).
                    ┌─────────────────────────────────────┐
                    │                                     │
    ┌───────────┐   │   ┌─────────────────────────────┐   │   ┌───────────┐
    │  REST API │───┼──►│          PORT               │   │   │  Database │
    │  Adapter  │   │   │    (Input Interface)        │   │   │  Adapter  │
    └───────────┘   │   └───────────┬─────────────────┘   │   └───────────┘
                    │               │                     │         ▲
    ┌───────────┐   │               ▼                     │         │
    │   CLI     │───┼──►┌─────────────────────────────┐   │   ┌─────┴─────┐
    │  Adapter  │   │   │                             │   │   │   PORT    │
    └───────────┘   │   │      DOMAIN / CORE          │◄──┼───│  (Output) │
                    │   │    (Business Logic)         │   │   └───────────┘
    ┌───────────┐   │   │                             │   │         │
    │   gRPC    │───┼──►└─────────────────────────────┘   │         ▼
    │  Adapter  │   │               │                     │   ┌───────────┐
    └───────────┘   │               ▼                     │   │   Email   │
                    │   ┌─────────────────────────────┐   │   │  Adapter  │
                    │   │          PORT               │───┼──►└───────────┘
                    │   │    (Output Interface)       │   │
                    │   └─────────────────────────────┘   │
                    │                                     │
                    └─────────────────────────────────────┘
# 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

┌─────────────────────────────────────────────────────────────────┐
│                        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

# 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

Order Service         Payment Service       Inventory Service
     │                      │                      │
     │   OrderCreated       │                      │
     ├─────────────────────►│                      │
     │                      │   PaymentProcessed   │
     │                      ├─────────────────────►│
     │                      │                      │
     │                      │   InventoryReserved  │
     │◄─────────────────────┼──────────────────────┤
     │                      │                      │
     │  OrderConfirmed      │                      │
     │                      │                      │

Compensation (if Inventory fails):
     │                      │   InventoryFailed    │
     │◄─────────────────────┼──────────────────────┤
     │   RefundPayment      │                      │
     ├─────────────────────►│                      │
     │   OrderCancelled     │                      │

Orchestration-Based Saga

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

                                    ┌─────────────────┐
                                    │  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)

┌──────────┐      ┌─────────────────┐
│  Mobile  │─────►│   Mobile BFF    │────┐
│   App    │      └─────────────────┘    │
└──────────┘                             │    ┌─────────────────┐
                                         ├───►│    Services     │
┌──────────┐      ┌─────────────────┐    │    └─────────────────┘
│   Web    │─────►│    Web BFF      │────┤
│   App    │      └─────────────────┘    │
└──────────┘                             │

┌──────────┐      ┌─────────────────┐    │
│  Admin   │─────►│   Admin BFF     │────┘
│  Panel   │      └─────────────────┘
└──────────┘

Circuit Breaker Pattern

Prevent cascading failures in distributed systems.
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

┌─────────────────────────────────────────────────────────────────┐
│                         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.
# 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

PatternComplexityScalabilityTeam SizeUse Case
Monolith🟢 Low🟡 MediumSmallMVPs, Startups
Modular Monolith🟡 Medium🟡 MediumSmall-MediumGrowing systems
Microservices🔴 High🟢 HighLargeComplex domains
Event-Driven🔴 High🟢 HighMedium+Real-time, Async
Serverless🟡 Medium🟢 HighSmall-MediumSporadic workloads
Layered🟢 Low🟡 MediumAnyTraditional apps
Hexagonal🟡 Medium🟡 MediumMediumTestable, 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.