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

20 KiB

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:

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

{
  "eventType": "OrderPlaced",
  "orderId": "12345",
  "timestamp": "2023-06-15T10:30:00Z"
}

Event-Carried State Transfer:

{
  "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:

[
  {"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:

// 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:

// 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:

// 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:

// 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 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):

// 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):

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:

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

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:

[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