mjohnson518 avatar

architecture-patterns

Architectural patterns and design principles for scalable, maintainable systems. Use when designing

by mjohnson518|Open Source

Architecture Patterns Skill

Purpose

Guide architectural decisions with proven patterns for building scalable, maintainable, and testable systems.

Core Architectural Styles

Clean Architecture (Onion Architecture)

┌─────────────────────────────────────────────────────────────┐
│                    Frameworks & Drivers                      │
│  ┌─────────────────────────────────────────────────────────┐│
│  │                 Interface Adapters                       ││
│  │  ┌─────────────────────────────────────────────────────┐││
│  │  │              Application Business Rules              │││
│  │  │  ┌─────────────────────────────────────────────────┐│││
│  │  │  │          Enterprise Business Rules               ││││
│  │  │  │              (Domain/Entities)                   ││││
│  │  │  └─────────────────────────────────────────────────┘│││
│  │  └─────────────────────────────────────────────────────┘││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘

Key Principles:

  • Dependencies point inward
  • Inner layers know nothing about outer layers
  • Domain entities are framework-agnostic

Layer Responsibilities:

LayerContainsDepends On
EntitiesBusiness objects, domain logicNothing
Use CasesApplication-specific logicEntities
AdaptersControllers, gateways, presentersUse Cases, Entities
FrameworksWeb framework, database, UIAll inner layers

Hexagonal Architecture (Ports & Adapters)

                    ┌──────────────┐
                    │   Driving    │
                    │   Adapter    │
                    │  (REST API)  │
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │    Port      │
                    │  (Interface) │
                    └──────┬───────┘
                           │
┌──────────────────────────▼──────────────────────────┐
│                    Application Core                  │
│  ┌────────────────────────────────────────────────┐ │
│  │              Domain Model                       │ │
│  │         (Entities + Business Rules)             │ │
│  └────────────────────────────────────────────────┘ │
└──────────────────────────┬──────────────────────────┘
                           │
                    ┌──────▼───────┐
                    │    Port      │
                    │  (Interface) │
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │   Driven     │
                    │   Adapter    │
                    │  (Database)  │
                    └──────────────┘

Key Concepts:

  • Ports: Interfaces defining how the core communicates
  • Driving Adapters: Things that call your application (HTTP, CLI, tests)
  • Driven Adapters: Things your application calls (DB, external APIs)

Domain-Driven Design (DDD)

Strategic Patterns:

PatternPurpose
Bounded ContextDefine clear boundaries between domains
Ubiquitous LanguageShared vocabulary between devs and domain experts
Context MappingDefine relationships between bounded contexts

Tactical Patterns:

PatternPurposeExample
EntityObject with identityUser, Order
Value ObjectImmutable, no identityMoney, Address
AggregateCluster of entitiesOrder + OrderItems
RepositoryAbstraction for persistenceUserRepository
Domain EventSomething that happenedOrderPlaced
Domain ServiceLogic not belonging to entityPaymentProcessor

Distributed System Patterns

Microservices

When to Use:

  • Large teams (> 10 developers)
  • Need independent deployability
  • Different scaling requirements per service
  • Polyglot persistence needed

When to Avoid:

  • Small teams
  • Simple domains
  • Tight latency requirements
  • Limited DevOps capability

Event-Driven Architecture

┌─────────┐     ┌──────────────┐     ┌─────────┐
│ Service │────▶│ Event Broker │────▶│ Service │
│    A    │     │  (Kafka/SQS) │     │    B    │
└─────────┘     └──────────────┘     └─────────┘
      │                                    │
      └──────────── Events ────────────────┘

Patterns:

  • Event Sourcing: Store all changes as events
  • CQRS: Separate read and write models
  • Saga: Manage distributed transactions

API Gateway Pattern

┌─────────┐     ┌─────────────┐     ┌───────────┐
│ Client  │────▶│ API Gateway │────▶│ Service A │
└─────────┘     └──────┬──────┘     ├───────────┤
                       │            │ Service B │
                       └───────────▶├───────────┤
                                    │ Service C │
                                    └───────────┘

Gateway Responsibilities:

  • Authentication/Authorization
  • Rate limiting
  • Request routing
  • Load balancing
  • Response caching

Resilience Patterns

Circuit Breaker

enum CircuitState {
  CLOSED,   // Normal operation
  OPEN,     // Failing, reject calls
  HALF_OPEN // Testing recovery
}

class CircuitBreaker {
  private state = CircuitState.CLOSED;
  private failures = 0;
  private threshold = 5;

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === CircuitState.OPEN) {
      throw new CircuitOpenError();
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
}

Retry with Exponential Backoff

async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3
): Promise<T> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await sleep(Math.pow(2, i) * 1000); // 1s, 2s, 4s
    }
  }
}

Bulkhead

Isolate failures to prevent cascade:

class Bulkhead {
  private semaphore: Semaphore;

  constructor(maxConcurrent: number) {
    this.semaphore = new Semaphore(maxConcurrent);
  }

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    await this.semaphore.acquire();
    try {
      return await fn();
    } finally {
      this.semaphore.release();
    }
  }
}

Data Patterns

Repository Pattern

interface UserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<void>;
  delete(id: string): Promise<void>;
}

// Implementation details hidden
class PostgresUserRepository implements UserRepository {
  // ...database-specific code
}

Unit of Work

interface UnitOfWork {
  users: UserRepository;
  orders: OrderRepository;

  commit(): Promise<void>;
  rollback(): Promise<void>;
}

CQRS (Command Query Responsibility Segregation)

┌──────────────┐         ┌──────────────────┐
│   Commands   │────────▶│   Write Model    │
│  (Create,    │         │   (Normalized)   │
│   Update)    │         └────────┬─────────┘
└──────────────┘                  │
                                  │ Sync
                                  ▼
┌──────────────┐         ┌──────────────────┐
│   Queries    │────────▶│   Read Model     │
│  (GetById,   │         │  (Denormalized)  │
│   Search)    │         └──────────────────┘
└──────────────┘

When to Use What

ScenarioRecommended Pattern
Complex domain logicDDD + Clean Architecture
High scalabilityMicroservices + Event-Driven
Simple CRUDMVC / Layered Architecture
Real-time updatesEvent Sourcing + CQRS
Legacy migrationStrangler Fig Pattern
API aggregationBFF (Backend for Frontend)

Anti-Patterns to Avoid

Anti-PatternProblemSolution
Big Ball of MudNo structureIntroduce bounded contexts
Distributed MonolithCoupled servicesTrue service boundaries
Anemic Domain ModelLogic in servicesRich domain model
Golden HammerOne pattern for allRight tool for job
Premature AbstractionOver-engineeringYAGNI, iterate

Decision Framework

Before choosing an architecture:

  1. What are the scaling requirements?

    • Users, requests, data volume
  2. What is the team size/structure?

    • Small team → simpler architecture
    • Multiple teams → clear boundaries
  3. What are the consistency requirements?

    • Strong consistency → synchronous
    • Eventual consistency → event-driven
  4. What is the expected rate of change?

    • High change → modular, loosely coupled
    • Stable → simpler is better
  5. What are the operational capabilities?

    • Limited DevOps → monolith
    • Strong DevOps → microservices possible