skills/software-architecture-design/references/modern-patterns.md

706 lines
20 KiB
Markdown

# Modern Software Architecture Patterns
Comprehensive guide to contemporary architecture patterns based on industry trends and practices.
## Top Architecture Patterns
### 1. Microservices Architecture
**When to use**:
- Multiple independent teams
- Need independent deployment and scaling
- Different technologies for different services
- Clear bounded contexts
**Structure**:
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Service │ │ Service │ │ Service │
│ A │ │ B │ │ C │
│ (Node.js) │ │ (Python) │ │ (Go) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
└─────────────────┴─────────────────┘
┌────────▼────────┐
│ API Gateway │
│ (Kong/Nginx) │
└─────────────────┘
```
**Best practices**:
- **Service discovery**: Use Consul, Eureka, or Kubernetes DNS
- **API Gateway**: Single entry point, authentication, rate limiting
- **Communication**: REST for synchronous, message queues for async
- **Data**: Each service owns its database (no shared DB)
- **Deployment**: Containerization (Docker) + orchestration (Kubernetes)
**Challenges**:
- Distributed system complexity
- Network latency and failures
- Data consistency across services
- Testing and debugging
- Operational overhead
**Mitigation**:
```yaml
# Service mesh (Istio/Linkerd) for:
- Service-to-service authentication
- Load balancing
- Circuit breaking
- Distributed tracing
- Metrics collection
```
### 2. Event-Driven Architecture (EDA)
**When to use**:
- Real-time data processing
- Asynchronous workflows
- Decoupled systems
- High scalability requirements
**Structure**:
```
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Producer │──────▶│ Event Broker │──────▶│ Consumer │
│ │ │ (Kafka/RabbitMQ) │ │
└──────────┘ └──────────────┘ └──────────┘
├──────▶ Consumer 2
└──────▶ Consumer 3
```
**Event patterns**:
**Event Notification**:
```json
{
"eventType": "OrderPlaced",
"orderId": "12345",
"timestamp": "2023-06-15T10:30:00Z"
}
```
**Event-Carried State Transfer**:
```json
{
"eventType": "OrderPlaced",
"orderId": "12345",
"customer": {"id": "C123", "name": "John"},
"items": [{"id": "P456", "qty": 2}],
"total": 99.99,
"timestamp": "2023-06-15T10:30:00Z"
}
```
**Event Sourcing**:
```json
[
{"event": "OrderCreated", "orderId": "12345", "seq": 1},
{"event": "ItemAdded", "orderId": "12345", "itemId": "P456", "seq": 2},
{"event": "OrderPaid", "orderId": "12345", "amount": 99.99, "seq": 3}
]
```
**Best practices**:
- **Idempotency**: Handle duplicate events gracefully
- **Schema evolution**: Use versioned event schemas
- **Error handling**: Dead letter queues for failed events
- **Monitoring**: Track event lag and processing times
- **Ordering**: Use partition keys for ordered processing
**Tools**:
- Apache Kafka - High-throughput distributed streaming
- RabbitMQ - Flexible message broker
- AWS EventBridge - Serverless event bus
- Google Pub/Sub - Global messaging service
### 3. Serverless Architecture
**When to use**:
- Variable/unpredictable load
- Event-driven workloads
- Rapid development and deployment
- Cost optimization (pay per use)
**Structure**:
```
┌─────────┐ ┌──────────────┐ ┌─────────┐
│ Event │─────▶│ Function │─────▶│ Store │
│ Source │ │ (Lambda/CF) │ │ (DynamoDB)
└─────────┘ └──────────────┘ └─────────┘
Event Sources:
- API Gateway (HTTP)
- S3 (file upload)
- DynamoDB Streams
- EventBridge (scheduled)
- SQS/SNS (messaging)
```
**Best practices**:
- **Cold start mitigation**: Keep functions warm with provisioned concurrency
- **Stateless design**: Use external state stores (Redis, DynamoDB)
- **Granular functions**: Single responsibility (≤300 LOC)
- **Resource limits**: Configure memory and timeout appropriately
- **Observability**: Use X-Ray, CloudWatch, or DataDog
**Example - AWS Lambda**:
```javascript
// Optimized function structure
export const handler = async (event) => {
// Input validation
const { userId, action } = JSON.parse(event.body);
// Business logic
const result = await processUserAction(userId, action);
// Response
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result)
};
};
// Keep external connections alive (outside handler)
const db = initDatabase();
```
**Cost optimization**:
- Use ARM-based functions (Graviton) - 20% cheaper
- Right-size memory allocation
- Use step functions for orchestration
- Implement caching to reduce invocations
### 4. Layered (N-Tier) Architecture
**When to use**:
- Monolithic applications
- Clear separation of concerns needed
- Team familiar with traditional patterns
- Moderate complexity
**Classic layers**:
```
┌──────────────────────────┐
│ Presentation Layer │ ← Controllers, Views, API endpoints
├──────────────────────────┤
│ Business Logic Layer │ ← Services, Domain models
├──────────────────────────┤
│ Data Access Layer │ ← Repositories, ORM
├──────────────────────────┤
│ Database Layer │ ← PostgreSQL, MongoDB
└──────────────────────────┘
```
**Dependency rule**: Outer layers depend on inner layers only
**Example structure**:
```
src/
├── controllers/ # HTTP request handlers
│ └── userController.js
├── services/ # Business logic
│ └── userService.js
├── repositories/ # Data access
│ └── userRepository.js
├── models/ # Domain models
│ └── user.js
└── database/ # DB configuration
└── connection.js
```
**Best practices**:
- **Dependency injection**: Pass dependencies, don't hardcode
- **Interface segregation**: Define clear contracts between layers
- **Error propagation**: Handle errors at appropriate layer
- **Transaction management**: Handle at service layer
### 5. Hexagonal Architecture (Ports & Adapters)
**When to use**:
- Need high testability
- Multiple interfaces (REST, GraphQL, CLI)
- Business logic must be technology-agnostic
- Long-term maintainability priority
**Structure**:
```
┌─────────────────────────┐
│ Application Core │
│ (Business Logic) │
│ │
│ ┌─────────────────┐ │
│ │ Domain Model │ │
│ └─────────────────┘ │
└────────┬──────┬──────────┘
│ │
┌────────────┘ └────────────┐
│ │
┌───▼──────┐ ┌─────▼────┐
│ Ports │ │ Ports │
│ (Input) │ │ (Output) │
└───┬──────┘ └─────┬────┘
│ │
┌───▼──────────┐ ┌────────▼─────┐
│ Adapters │ │ Adapters │
│ REST, GraphQL│ │ DB, External │
└──────────────┘ └──────────────┘
```
**Implementation**:
```typescript
// Core domain (technology-agnostic)
interface UserRepository {
findById(id: string): Promise<User>;
save(user: User): Promise<void>;
}
class UserService {
constructor(private userRepo: UserRepository) {}
async activateUser(id: string): Promise<User> {
const user = await this.userRepo.findById(id);
user.activate(); // Business logic
await this.userRepo.save(user);
return user;
}
}
// Adapters (technology-specific)
class PostgresUserRepository implements UserRepository {
async findById(id: string): Promise<User> {
const row = await db.query('SELECT * FROM users WHERE id = $1', [id]);
return User.fromDatabase(row);
}
async save(user: User): Promise<void> {
await db.query('UPDATE users SET ...', user.toDatabase());
}
}
class RestAdapter {
constructor(private userService: UserService) {}
async handleActivateUser(req, res) {
const user = await this.userService.activateUser(req.params.id);
res.json(user);
}
}
```
### 6. CQRS (Command Query Responsibility Segregation)
**When to use**:
- Read and write patterns are very different
- High read:write ratio
- Complex reporting requirements
- Need independent scaling of reads and writes
**Structure**:
```
┌─────────────┐
│ Command │
│ (Write) │
└──────┬──────┘
┌─────────▼──────────┐
│ Write Database │
│ (Normalized) │
└─────────┬──────────┘
│ (sync/async)
┌─────────▼──────────┐
│ Read Database │
│ (Denormalized) │
└─────────┬──────────┘
┌──────▼──────┐
│ Query │
│ (Read) │
└─────────────┘
```
**Example**:
```typescript
// Command (Write)
class CreateOrderCommand {
constructor(
public customerId: string,
public items: OrderItem[]
) {}
}
class OrderCommandHandler {
async handle(cmd: CreateOrderCommand) {
const order = new Order(cmd.customerId, cmd.items);
await writeDb.orders.save(order);
// Publish event for read model update
await eventBus.publish(new OrderCreatedEvent(order));
}
}
// Query (Read)
class GetCustomerOrdersQuery {
constructor(public customerId: string) {}
}
class OrderQueryHandler {
async handle(query: GetCustomerOrdersQuery) {
// Read from optimized read model
return await readDb.customerOrders.find({
customerId: query.customerId
});
}
}
// Event handler to sync read model
class OrderCreatedEventHandler {
async handle(event: OrderCreatedEvent) {
// Update denormalized read model
await readDb.customerOrders.insert({
customerId: event.customerId,
orderId: event.orderId,
total: event.total,
// ... optimized for reads
});
}
}
```
### 7. Modular Monolith
**When to use**:
- Team size 5-30 developers
- Want clear boundaries without microservices overhead
- Need faster development than microservices
- Shared domain concepts across modules
**Structure**:
```
monolith/
├── modules/
│ ├── orders/
│ │ ├── api/ # Public interface
│ │ ├── domain/ # Business logic (private)
│ │ └── infrastructure/ # DB, external services (private)
│ ├── payments/
│ │ ├── api/
│ │ ├── domain/
│ │ └── infrastructure/
│ └── shipping/
│ ├── api/
│ ├── domain/
│ └── infrastructure/
└── shared/
├── database/
└── messaging/
```
**Module boundaries**:
```typescript
// orders/api/OrdersModule.ts (public API)
export class OrdersModule {
static async createOrder(data: CreateOrderDTO): Promise<Order> {
// Implementation hidden
}
static async getOrder(id: string): Promise<Order> {
// Implementation hidden
}
}
// payments/PaymentsService.ts
import { OrdersModule } from '../orders/api/OrdersModule';
class PaymentsService {
async processPayment(orderId: string) {
// Use public API only, no direct access to orders internals
const order = await OrdersModule.getOrder(orderId);
// ...
}
}
```
**Advantages over microservices**:
- Single deployment (simpler CI/CD)
- No network latency between modules
- Shared transactions possible
- Easier refactoring (can extract to microservice later)
### 8. Micro-Frontend Architecture
**When to use**:
- Multiple teams working on different features
- Different technology stacks for different parts
- Independent deployment of UI components
- Large-scale front-end applications
**Approaches**:
**A) Server-side composition (SSR)**:
```nginx
# Nginx routes different paths to different apps
location /products {
proxy_pass http://products-frontend:3000;
}
location /checkout {
proxy_pass http://checkout-frontend:3001;
}
```
**B) Build-time composition (Module Federation)**:
```javascript
// Webpack Module Federation
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList'
},
shared: ['react', 'react-dom']
})
]
};
// Host app imports remote component
const ProductList = React.lazy(() => import('products/ProductList'));
```
**C) Runtime composition (Single-SPA)**:
```javascript
import { registerApplication, start } from 'single-spa';
registerApplication({
name: 'products',
app: () => import('./products/main.js'),
activeWhen: location => location.pathname.startsWith('/products')
});
registerApplication({
name: 'checkout',
app: () => import('./checkout/main.js'),
activeWhen: '/checkout'
});
start();
```
### 9. Service Mesh Architecture
**When to use**:
- Microservices at scale (10+ services)
- Need advanced traffic management
- Security and observability are critical
- Polyglot microservices
**Structure**:
```
Service A ──▶ Sidecar Proxy (Envoy)
│ ──▶ Sidecar Proxy ──▶ Service B
└─ Control Plane (Istio)
├─ Traffic management
├─ Security (mTLS)
└─ Observability
```
**Features**:
- **Traffic management**: Load balancing, circuit breaking, retries
- **Security**: Mutual TLS, authorization policies
- **Observability**: Distributed tracing, metrics, logging
**Example - Istio**:
```yaml
# Virtual Service (traffic routing)
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- match:
- headers:
user-agent:
regex: '.*Chrome.*'
route:
- destination:
host: reviews
subset: v2
- route:
- destination:
host: reviews
subset: v1
# Circuit breaker
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: reviews
spec:
host: reviews
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
http:
http1MaxPendingRequests: 1
maxRequestsPerConnection: 2
outlierDetection:
consecutive5xxErrors: 5
interval: 30s
baseEjectionTime: 30s
```
### 10. Edge Computing Architecture
**When to use**:
- Need ultra-low latency
- IoT applications
- Content delivery
- Real-time processing
**Structure**:
```
┌─────────────────────────────────────────┐
│ Cloud (Central) │
│ - Data aggregation │
│ - ML model training │
│ - Long-term storage │
└────────────┬────────────────────────────┘
┌────────┴────────┐
│ │
┌───▼──────┐ ┌─────▼────┐
│ Edge │ │ Edge │
│ Node 1 │ │ Node 2 │
│ - Process│ │ - Process│
│ - Cache │ │ - Cache │
│ - Filter │ │ - Filter │
└───┬──────┘ └─────┬────┘
│ │
┌───▼──┐ ┌───▼──┐
│ IoT │ │ IoT │
│Device│ │Device│
└──────┘ └──────┘
```
**Use cases**:
- CDN edge workers (Cloudflare Workers, Lambda@Edge)
- Smart city sensors
- Industrial IoT
- Autonomous vehicles
**Example - Cloudflare Worker**:
```javascript
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
// Process at edge (near user)
const cache = caches.default;
let response = await cache.match(request);
if (!response) {
// Fetch from origin if not cached
response = await fetch(request);
// Cache at edge
event.waitUntil(cache.put(request, response.clone()));
}
return response;
}
```
## Architecture Selection Decision Tree
```
Start: What are you building?
├─ Simple CRUD app
│ └─ Use: Layered Architecture
├─ Need independent team scaling?
│ ├─ Yes → Need independent deployments?
│ │ ├─ Yes → Use: Microservices
│ │ └─ No → Use: Modular Monolith
│ └─ No → Use: Layered or Hexagonal
├─ Event-driven requirements?
│ ├─ Primary pattern → Use: Event-Driven Architecture
│ └─ Secondary pattern → Add messaging to chosen architecture
├─ Unpredictable/variable load?
│ └─ Use: Serverless
├─ Different read/write patterns?
│ └─ Use: CQRS + Event Sourcing
└─ Multiple UI teams?
└─ Use: Micro-Frontends
```
## Anti-Patterns to Avoid
### 1. Distributed Monolith
Microservices that are tightly coupled:
```
[FAIL] Service A calls Service B, which calls Service C, which calls Service A
[OK] Use message queues or events to decouple
```
### 2. God Service
One service that does everything:
```
[FAIL] UserOrderPaymentShippingService
[OK] UserService, OrderService, PaymentService, ShippingService
```
### 3. Anemic Domain Model
Models with no behavior, just getters/setters:
```typescript
[FAIL] // Anemic
class Order {
items: OrderItem[];
getItems() { return this.items; }
setItems(items) { this.items = items; }
}
[OK] // Rich domain model
class Order {
private items: OrderItem[];
addItem(item: OrderItem) {
this.validateItem(item);
this.items.push(item);
this.recalculateTotal();
}
canBeCancelled(): boolean {
return this.status === 'pending' && !this.isPaid;
}
}
```
### 4. Chatty APIs
Too many network calls:
```
[FAIL] GET /users/1, GET /users/1/orders, GET /orders/1/items
[OK] GET /users/1?include=orders.items
```
## Resources
- Martin Fowler - Architecture Patterns
- Microsoft Azure - Architecture Center
- AWS - Well-Architected Framework
- Google Cloud - Architecture Framework
- Microservices.io - Pattern catalog