Summary
A technical overview of core architectural patterns for API design and service integration in distributed systems. These patterns address different concerns in system architecture: centralised access control, client-specific optimizations, read/write segregation, domain separation, data aggregation, and interface simplification.
Core Patterns Covered:
API Gateway: Centralised entry point for routing and managing API requests
Backend for Frontend (BFF): Client-specific backend adaptations
Command-Query Responsibility Segregation (CQRS): Separation of read and write operations
API Design in Bounded Contexts: Domain-aligned API structuring
Aggregator Pattern: Composition of data from multiple services
API Facade Pattern: Simplification layer for complex or legacy systems
Each pattern serves distinct architectural needs and can be implemented independently or combined based on system requirements. The guide includes implementation considerations, use cases, and decision criteria for pattern selection.
Patterns
API Gateway
Client → API Gateway → Microservices/Bounded Contexts
Purpose: Single entry point that routes requests to appropriate services
When to use:
Need centralised authentication/authorization
Want to reduce round trips from client
Need request/response transformation
Want to hide internal service structure from clients
Example use case: Mobile app needs data from multiple services but wants single API endpoint
Backend for Frontend (BFF)
Mobile App → Mobile BFF → Services
Web App → Web BFF → Services
Desktop → Desktop BFF → Services
Purpose: Tailored backend for specific frontend needs
When to use:
Different clients need different data shapes
Want to optimise for specific client needs
Need to reduce unnecessary data transfer
Example use case: Mobile app needs lighter payload than web app
CQRS (Command Query Responsibility Segregation)
Command side (writes)
class CampaignCommandAPI:
def deactivate_campaign(self, id: str):
command = DeactivateCommand(id)
command_bus.send(command)
# Query side (reads)
class CampaignQueryAPI:
def get_campaign(self, id: str):
return query_db.get_campaign_view(id)
Purpose: Separate read and write operations
When to use:
Read and write operations have very different requirements
Need different optimization for reads vs writes
Complex reporting needs
Example use case: High-read, low-write system like product catalog
API Design in Bounded Contexts
Shipping Context API
class ShippingAPI:
def create_shipment(self, order_id: str):
pass
# Payment Context API
class PaymentAPI:
def process_payment(self, order_id: str):
pass
Purpose: Each bounded context has its own API aligned with its domain
When to use:
Different teams own different domains
Need clear separation of concerns
Want to maintain domain integrity
Example use case: E-commerce system with separate teams for orders, shipping, payments
Aggregator Pattern
Client → Aggregator → Multiple Services
Purpose: Combines data from multiple services into single response
When to use:
Need to reduce client requests
Complex data aggregation requirements
Composite services
Example use case: E-commerce product page needing product details, inventory, pricing, reviews, and related products from different services in one call
API Facade Pattern
Client → Facade → Legacy/Complex System
Purpose: Simplifies complex APIs or legacy systems
When to use:
Modernising legacy systems
Simplifying complex interfaces
Creating consistent API layer
Example use case: Modern REST API interface for a legacy system, converting complex multi-step operations into simple, single endpoints
Comparison Table
Pattern | Main Benefit | Complexity | Best For
-----------------+------------------------|------------+------------------
API Gateway | Centralization | Medium | Microservices
BFF | Client Optimization | High | Multiple Clients
CQRS | Performance | High | Complex Reads
Bounded Context | Domain Separation | Medium | Large Teams
Aggregator | Data Composition | Medium | Complex Data
Facade | Interface Simpl. | Low | Legacy Systems
Decision Framework
Start with Bounded Contexts if:
Large system
Multiple teams
Clear domain separation
Add API Gateway if:
Multiple clients
Need central authentication
Want to hide internal structure
Add BFF if:
Different client needs
Performance critical
Client-specific optimization needed
Add CQRS if:
Read/write patterns very different
Complex reporting needs
Performance bottlenecks
Add Aggregator if:
Multiple service calls needed for single operation
Need to reduce client-side data assembly
Complex data relationships across services
Add Façade if:
Working with legacy systems
Need to simplify complex APIs
Want to standardize interfaces
Common Combinations
API Gateway + BFF:
Mobile → Mobile BFF → API Gateway → Services
Web → Web BFF → API Gateway → Services
API Gateway + CQRS:
Client → API Gateway → Command API
→ Query API
API Gateway + Aggregator:
Client → API Gateway → Aggregator → Multiple Services
Facade + Legacy Integration:
Client → API Gateway → Facade → Legacy System
→ Modern Services
Complete Stack:
Mobile → Mobile BFF → API Gateway → Aggregator → Services
Web → Web BFF ↗ → Facade → Legacy Systems
→ Command/Query APIs → Bounded Contexts
Tips
Start simple
Add complexity only when needed
Each pattern adds maintenance overhead
Patterns can be combined as system grows