Architectural Patterns — Cheat Sheet
System Design · 7 topics. Download the PDF or the Instagram carousel and share it.
Microservices Architecture
Microservices decompose a system into small, independently deployable services, each owning a specific business capability. They enable team autonomy, independent scaling, and technology flexibility at the cost of distributed-systems complexity.
- ✓Each microservice owns one business capability and its own database.
- ✓Services communicate via REST/gRPC (sync) or events/queues (async).
- ✓Benefits: independent deployment, team autonomy, per-service scaling, tech flexibility.
- ✓Costs: distributed transactions, debugging complexity, network overhead, operational burden.
- ✓Start with a monolith; extract microservices when team size and scale justify the complexity.
// Monolith — single deployable unit // ┌─────────────────────────────────┐ // │ Monolith │ // │ ┌──────┐ ┌──────┐ ┌──────┐ │ // │ │Users │ │Orders│ │Pay │ │ // │ │Module│ │Module│ │Module│ │ // │ └──┬───┘ └──┬───┘ └──┬───┘ │ // │ └────────┴────────┘ │ // │ Shared Database │ // └─────────────────────────────────┘ // Microservices — independent services // ┌──────────┐ ┌──────────┐ ┌──────────┐ // │ User Svc │ │ Order Svc │ │ Pay Svc │ // │ (Java) │ │ (Java) │ │ (Go) │ // └─────┬────┘ └─────┬────┘ └─────┬────┘ // │ │ │ // Users DB Orders DB Payments DB // (PostgreSQL) (PostgreSQL) (DynamoDB) // Communication: // Sync: REST / gRPC (request-response) // Async: Kafka / SQS (event-driven)
CQRS (Command Query Responsibility Segregation)
CQRS separates read (query) and write (command) models into different data stores optimised for each. Writes go to a normalised DB; reads come from a denormalised, pre-computed view.
- ✓CQRS separates write (command) and read (query) into different models and stores.
- ✓Write model: normalised, enforces business rules. Read model: denormalised, optimised for queries.
- ✓Events propagate changes from write to read side — eventual consistency.
- ✓Enables independent scaling: more read replicas for high-read workloads.
- ✓Use CQRS when read and write patterns are vastly different — not for simple CRUD apps.
// CQRS architecture // // Client // │ Command (write) │ Query (read) // ▼ ▼ // ┌──────────────┐ ┌──────────────┐ // │ Command Side │ │ Query Side │ // │ (Write Model) │ │ (Read Model) │ // │ OrderService │ │ OrderView │ // └──────┬───────┘ └──────▲───────┘ // │ write │ read // ▼ │ // ┌──────────┐ event ┌──────────┐ // │ Orders DB │ ────────►│ Read DB │ // │(PostgreSQL)│ (Kafka) │(Elasticsearch)│ // └──────────┘ └──────────┘ // (normalised) (denormalised, fast queries) // Write model: normalised, enforces business rules // Read model: denormalised, optimised for UI queries // Event bus: propagates changes from write → read
Event Sourcing
Event sourcing stores the state of a system as a sequence of immutable events rather than mutable rows. Current state is derived by replaying events. It provides a complete audit trail and enables temporal queries.
- ✓Event sourcing stores all state changes as immutable events — current state is derived by replay.
- ✓Provides complete audit trail, temporal queries, and natural integration with event-driven systems.
- ✓Snapshots prevent performance degradation from replaying long event histories.
- ✓Projections build read-optimised views from events — often combined with CQRS.
- ✓Event schema evolution is the biggest long-term challenge — plan for versioning from day one.
// Traditional DB — stores current state
// | account_id | balance |
// | A1 | 500.00 |
// Previous values are lost on UPDATE
// Event sourcing — stores all changes as events
// Event Store:
// | event_id | aggregate_id | type | data | timestamp |
// | 1 | A1 | Created | { owner: "Alice" }| 2025-01-01 |
// | 2 | A1 | Deposited | { amount: 1000 } | 2025-01-05 |
// | 3 | A1 | Withdrawn | { amount: 300 } | 2025-01-10 |
// | 4 | A1 | Deposited | { amount: 200 } | 2025-02-01 |
// | 5 | A1 | Withdrawn | { amount: 400 } | 2025-03-01 |
//
// Current balance = replay: 0 + 1000 - 300 + 200 - 400 = 500
// Balance on Jan 10? Replay events 1-3: 0 + 1000 - 300 = 700Saga Pattern
The Saga pattern manages distributed transactions across microservices using a sequence of local transactions with compensating actions for rollback, replacing traditional two-phase commit.
- ✓Saga replaces distributed transactions (2PC) with a sequence of local transactions + compensating actions.
- ✓Choreography: services react to events (decentralised, good for simple sagas).
- ✓Orchestration: central coordinator manages steps (better for complex multi-step sagas).
- ✓Compensating transactions must be idempotent — they may be triggered multiple times.
- ✓Sagas provide eventual consistency, not ACID — design for intermediate states being visible.
// Choreography saga — order placement
//
// 1. OrderService: create order (PENDING) → emit "OrderCreated"
// 2. PaymentService: listens "OrderCreated" → charge payment → emit "PaymentCompleted"
// 3. InventoryService: listens "PaymentCompleted" → reserve stock → emit "StockReserved"
// 4. OrderService: listens "StockReserved" → update order to CONFIRMED
//
// Failure at step 3 (out of stock):
// 3. InventoryService: emit "StockReservationFailed"
// 2. PaymentService: listens → refund payment → emit "PaymentRefunded"
// 1. OrderService: listens → cancel order → set status to CANCELLED
// Spring Boot choreography implementation
@KafkaListener(topics = "order-created")
public void onOrderCreated(OrderCreatedEvent event) {
try {
paymentGateway.charge(event.getUserId(), event.getTotal());
kafka.send("payment-completed", new PaymentCompletedEvent(event.getOrderId()));
} catch (PaymentFailedException e) {
kafka.send("payment-failed", new PaymentFailedEvent(event.getOrderId()));
}
}API Gateway
An API Gateway is a single entry point for all client requests. It handles routing, authentication, rate limiting, request aggregation, and protocol translation, shielding internal microservices from direct client access.
- ✓API Gateway is a single entry point — routes, authenticates, rate-limits, and aggregates.
- ✓Decouples clients from internal service topology — clients call one URL, gateway routes internally.
- ✓Backend for Frontend (BFF): separate gateways tailored for mobile, web, and third-party clients.
- ✓Popular options: Kong, NGINX, AWS API Gateway, Spring Cloud Gateway, Envoy.
- ✓Avoid making the gateway a bottleneck — keep it thin, push business logic to services.
// API Gateway architecture // // Mobile App Web App 3rd-party // │ │ │ // └─────┬─────┴──────────┘ // ▼ // ┌──────────────────┐ // │ API Gateway │ ← routing, auth, rate limit, aggregation // │ (Kong / NGINX) │ // └──┬──┬──┬──┬──────┘ // │ │ │ │ // ▼ ▼ ▼ ▼ // User Order Payment Search // Svc Svc Svc Svc // Gateway responsibilities: // 1. Routing: /api/users/** → User Service // 2. Auth: Validate JWT, attach user context // 3. Rate limit: 100 req/min per API key // 4. Aggregation: /api/dashboard → calls 3 services, merges response // 5. Transformation: REST → gRPC (for internal services) // 6. SSL termination: HTTPS at gateway, HTTP internally // 7. Logging: Centralised access logs, request tracing
Service Discovery
Service discovery enables services to find each other dynamically in a microservices environment where instances scale up/down and IPs change frequently. It replaces hardcoded addresses with a registry-based lookup.
- ✓Service discovery replaces hardcoded addresses with dynamic registry-based lookup.
- ✓Client-side discovery: client queries registry and load-balances (Eureka + Ribbon/Feign).
- ✓Server-side discovery: load balancer or DNS resolves service names transparently (Kubernetes Services).
- ✓Kubernetes provides built-in service discovery via DNS — no external registry needed.
- ✓Health checks ensure only healthy instances are discoverable.
// Client-side discovery (Eureka + Spring Cloud)
// 1. Service registers itself with Eureka
// 2. Client queries Eureka for "ORDER-SERVICE"
// 3. Client gets list of IPs: [10.0.1.5:8080, 10.0.1.6:8080]
// 4. Client picks one (round-robin, random, etc.)
// Server-side discovery (Kubernetes)
// 1. Pod registers via Kubernetes API (automatic)
// 2. Client calls: http://order-service:8080/api/orders
// 3. kube-proxy resolves DNS → one of the pod IPs
// 4. Transparent to the client
// Kubernetes Service (server-side discovery)
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service # matches pods with this label
ports:
- port: 8080
targetPort: 8080
type: ClusterIP # internal DNS: order-service.default.svc.cluster.localStrangler Fig Pattern
The Strangler Fig pattern incrementally migrates a monolith to microservices by gradually routing traffic from legacy endpoints to new services, eventually "strangling" the old system.
- ✓Strangler Fig enables incremental monolith-to-microservices migration without big-bang rewrite.
- ✓A proxy/gateway routes traffic — new endpoints go to microservices, rest stays on monolith.
- ✓Each migrated endpoint can be rolled back to the monolith if issues arise.
- ✓Database splitting is the hardest part — use CDC (Debezium) for transition period sync.
- ✓Combined with feature flags and canary deployments for low-risk cutover.
// Strangler Fig migration phases
//
// Phase 1: Proxy in front of monolith (everything goes to monolith)
// Client → API Gateway → Monolith (all endpoints)
//
// Phase 2: Extract UserService
// Client → API Gateway
// /api/users/** → NEW UserService (microservice)
// /api/* → Monolith (everything else)
//
// Phase 3: Extract OrderService
// Client → API Gateway
// /api/users/** → UserService
// /api/orders/** → NEW OrderService
// /api/* → Monolith (shrinking)
//
// Phase N: Monolith is empty → decommission
// Client → API Gateway → All microservices
// NGINX routing during migration
upstream monolith { server monolith:8080; }
upstream user_service { server user-svc:8080; }
upstream order_service { server order-svc:8080; }
server {
location /api/users/ { proxy_pass http://user_service; }
location /api/orders/ { proxy_pass http://order_service; }
location / { proxy_pass http://monolith; } # default: monolith
}