πŸ—οΈ Introduction

Software architecture patterns are reusable solutions to common problems in software design. At Aevum Encyclopedia, we use a combination of architectural patterns to build scalable, maintainable, and resilient systems that serve millions of articles across 140+ languages.

This document catalogs the patterns currently in use across our platform, provides guidance on their appropriate application, and documents the trade-offs we've encountered in production. Each pattern includes:

  • A clear description of the pattern and its purpose
  • Visual diagrams showing the structural relationships
  • Concrete examples from the Aevum Encyclopedia codebase
  • When to use β€” and when not to use β€” the pattern
  • Associated anti-patterns and common pitfalls
ℹ️

Who Should Read This

This guide is intended for software engineers, architects, and technical leads working on or contributing to Aevum Encyclopedia services. Foundational knowledge of distributed systems is assumed.

πŸ“Š Pattern Comparison Overview

Before diving into individual patterns, here's a high-level comparison of the primary architectural patterns used across our platform:

Pattern Use Case Complexity Scalability Coupling
Layered Simple to moderate services Low Vertical Vertical coupling
Microservices Large, distributed systems High Horizontal Low (network)
Event-Driven Async workflows, real-time Medium Horizontal Very low
CQRS Complex read/write separation Medium Independent Low
Hexagonal Testable, portable business logic Medium N/A Inbound dependency only
Event Sourcing Audit trails, temporal queries High Horizontal Very low
Serverless Event-triggered, variable load Low Auto-scale Very low

πŸ“š Layered (N-Tier) Architecture

πŸ“š
Layered Architecture
Also known as N-Tier or Onion Architecture
Low Complexity

The layered pattern organizes software into horizontal layers, each with a specific responsibility. Requests flow through each layer sequentially, and each layer only communicates with adjacent layers. This is our default pattern for new microservices unless a more specialized pattern is justified.

Used In
Search API, Auth Service
Primary Benefit
Separation of Concerns
Key Trade-off
Through-layer calls
Maturity
Production β€” Stable

Layer Diagram

Standard Layered Architecture
UI / Client Layer
Application / API Layer
Domain / Business Logic Layer
Data Access Layer
Infrastructure / Database

Example: Search Service Layered Structure

TypeScript
// Layered architecture example from our Search Service
// Layer 1: API Controller
class SearchController {
  constructor(private service: SearchService) {}
  async search(query: SearchQuery): Promise<SearchResults> {
    return this.service.executeSearch(query);
  }
}

// Layer 2: Application Service
class SearchService {
  constructor(
    private repo: ArticleRepository,
    private cache: SearchCache
  ) {}
  async executeSearch(query: SearchQuery): Promise<SearchResults> {
    const cached = await this.cache.get(query.id);
    if (cached) return cached;
    const results = await this.repo.search(query);
    this.cache.set(query.id, results);
    return results;
  }
}

// Layer 3: Repository (Data Access)
class ElasticsearchRepository implements ArticleRepository {
  async search(query: SearchQuery): Promise<SearchResults> {
    return this.client.search({ index: 'articles', body: query });
  }
}
⚠️

Common Pitfall: Transaction Script Anti-Pattern

Avoid putting business logic in controllers or creating "anemic" domain models. Each layer should have meaningful responsibilities, not just pass data through.

πŸ”— Microservices Architecture

πŸ”—
Microservices Architecture
Distributed, independently deployable services
High Complexity

The Aevum Encyclopedia platform is organized as a collection of ~45 microservices, each owning a specific domain and capable of independent deployment. Services communicate via synchronous HTTP/gRPC calls and asynchronous messaging through our event bus.

Used In
Entire Platform
Communication
gRPC + Kafka Events
Data Strategy
Database per Service
Team Model
Conway's Law Aligned

Service Communication Flow

Microservices Communication Pattern
πŸ–₯️ Client
↓
πŸšͺ API Gateway
↓ ↕ ↓
πŸ“ Content Svc
πŸ” Search Svc
πŸ‘€ Auth Svc
πŸ“Š Analytics
↓
πŸ“¨ Kafka Event Bus
↓
πŸ—„οΈ PostgreSQL
πŸ”Ž Elasticsearch
⚑ Redis
πŸ“„ MongoDB

Service Boundaries

Our services are bounded by Domain-Driven Design (DDD) strategic patterns. Each service maps to a bounded context:

  • Content Service β€” Article creation, editing, versioning
  • Search Service β€” Full-text search, faceting, ranking
  • Translation Service β€” AI-powered multi-language translation
  • Auth Service β€” User identity, OAuth, RBAC
  • Notification Service β€” Email, push, in-app notifications
  • Analytics Service β€” Page views, reading patterns, metrics
πŸ’‘

When to Create a New Service

Don't start with microservices. Start with a modular monolith and extract services when you have clear evidence of independent scaling needs, distinct team ownership, or different technology requirements.

⚑ Event-Driven Architecture

⚑
Event-Driven Architecture (EDA)
Asynchronous communication via events
Medium Complexity

Event-driven architecture decouples services by having them communicate through an event broker. Services publish events when state changes occur and subscribe to events of interest. This is the backbone of our real-time content pipeline and translation workflow.

Event Broker
Apache Kafka
Event Format
CloudEvents JSON
Delivery Guarantee
At-least-once
Key Use Case
Content Pipeline

Event Flow: Article Published

Content Publishing Event Chain
πŸ“ Content Service
β†’ publishes β†’
ArticlePublished
β†’
Kafka Topic
β†’ 3 consumers β†’
πŸ” Search Indexer
🌐 Translator
πŸ”” Notifier

Event Schema Example

JSON β€” CloudEvents
{
  "specversion": "1.0",
  "id": "evt_2f8d7a3b",
  "type": "aevum.content.article.published",
  "source": "content-service",
  "time": "2025-03-15T10:30:00Z",
  "data": {
    "articleId": "art_9f3c2e1a",
    "title": "Quantum Computing",
    "language": "en",
    "version": 14,
    "authorId": "usr_7b4e1d2c",
    "categoryIds": ["cat_science", "cat_technology"],
    "wordCount": 2847
  }
}
🚨

Idempotency is Mandatory

With at-least-once delivery, consumers may receive duplicate events. Every event handler must be idempotent β€” use event IDs, deduplication tables, or idempotency keys to ensure safe re-processing.

πŸ”„ Command Query Responsibility Segregation (CQRS)

πŸ”„
CQRS Pattern
Separate read and write models
Medium Complexity

CQRS separates the responsibility for updating data (Commands) from the responsibility for reading data (Queries). At Aevum Encyclopedia, we use CQRS primarily in the Content Service and Analytics Service, where read and write workloads differ significantly.

CQRS Architecture
πŸ–₯️ Client
↕
πŸ“€ Command Side
↓
Write Model
↓
Write DB (PostgreSQL)
πŸ“₯ Query Side
↓
Read Model
↓
Read DB (Elasticsearch)
async event sync β†’

Code Example: Command Handler

TypeScript β€” CQRS
// Command β€” modifies state
class PublishArticleCommand implements Command {
  readonly articleId: string;
  readonly title: string;
  readonly content: string;
  readonly authorId: string;
}

class PublishArticleHandler implements CommandHandler<PublishArticleCommand> {
  async execute(cmd: PublishArticleCommand): Promise<void> {
    // Validate, apply business rules, persist to write DB
    const article = await this.repo.findById(cmd.articleId);
    article.publish(cmd.title, cmd.content);
    await this.repo.save(article);
    await this.bus.publish(new ArticlePublishedEvent(article));
  }
}

// Query β€” reads optimized data
class GetArticleByIdQuery implements Query<ArticleDTO> {
  readonly articleId: string;
}

class GetArticleByIdHandler implements QueryHandler<GetArticleByIdQuery, ArticleDTO> {
  async execute(query: GetArticleByIdQuery): Promise<ArticleDTO> {
    // Read from optimized read model (Elasticsearch)
    return this.readModel.get(query.articleId);
  }
}
ℹ️

Eventual Consistency

CQRS introduces eventual consistency between read and write models. For Aevum Encyclopedia, our read-write propagation delay is typically under 500ms, which is acceptable for the encyclopedia use case. Never use CQRS where strong consistency is required.

⬑ Hexagonal Architecture (Ports & Adapters)

⬑
Hexagonal Architecture
Also known as Ports and Adapters
Medium Complexity

Hexagonal architecture isolates the core business logic from external concerns by defining ports (interfaces) and adapters (implementations). This pattern is used in all mission-critical services at Aevum Encyclopedia to ensure the domain logic remains testable and framework-independent.

Hexagonal Architecture Diagram
REST API
β†’
CLI
β†’
CORE DOMAIN
Business Logic
β¬… In Ports
Out Ports ➑
β†’
PostgreSQL
↑
Kafka
↑

Key Principles

  • Domain first: Business logic depends on nothing external
  • Inbound ports: Interfaces defined by the domain for how it can be used (use cases)
  • Outbound ports: Interfaces defined by the domain for external capabilities it needs
  • Adapters: Concrete implementations of ports (REST controllers, DB repositories, message producers)
  • Dependency inversion: Frameworks adapt to the domain, not vice versa
πŸ’‘

Testing Benefit

With hexagonal architecture, you can unit test your entire domain logic without databases, message queues, or network calls. Replace outbound port adapters with in-memory implementations for fast, reliable tests.

πŸ“ Event Sourcing

πŸ“
Event Sourcing
State derived from an immutable event log
High Complexity

Event sourcing stores state changes as a sequence of events rather than just the current state. This is used in the Article Versioning Service to maintain a complete audit trail of every edit, review, and approval β€” essential for a collaborative encyclopedia platform.

Used In
Article Versioning
Event Store
PostgreSQL + Kafka
Rehydration
Snapshot + Delta
Key Benefit
Full Audit Trail

Event Stream for an Article

Event Log β€” Article art_9f3c2e1a
seq:1
ArticleCreated
2025-01-10T08:00Z
seq:2
ContentEdited
2025-01-12T14:30Z
seq:3
ReviewerAssigned
2025-01-13T09:15Z
seq:4
ReviewApproved
2025-01-15T16:45Z
seq:5
ArticlePublished
2025-01-15T16:45Z
⚠️

Performance Consideration

Replaying thousands of events to reconstruct state can be slow. Use snapshots periodically (e.g., every 100 events) to reduce replay time. Our implementation snapshots after every 50 events.

☁️ Serverless Architecture

☁️
Serverless Architecture
Function-as-a-Service (FaaS) for event-triggered workloads
Low Complexity

Serverless functions are used for bursty, event-driven tasks that don't warrant dedicated service infrastructure. At Aevum Encyclopedia, serverless handles image processing, PDF generation, sitemap updates, and CDN cache invalidation.

Platform
AWS Lambda / GCP Cloud Run
Trigger Types
HTTP, S3, EventBridge, Timer
Timeout
Max 15 minutes
Cold Start
~200ms (Provisioned Concurrency)

When to Use Serverless

βœ… Good Fit

  • βœ“ Irregular or unpredictable traffic
  • βœ“ Event-driven processing
  • βœ“ Short-lived operations (<5min)
  • βœ“ Background batch jobs
  • βœ“ Prototyping / new features

❌ Poor Fit

  • βœ— Long-running processes
  • βœ— Steady, high-throughput APIs
  • βœ— Stateful connections (WebSockets)
  • βœ— Large memory requirements (>10GB)
  • βœ— Low-latency guarantees needed

πŸ•ΈοΈ Service Mesh

πŸ•ΈοΈ
Service Mesh Pattern
Infrastructure layer for service-to-service communication
High Complexity

We use Istio as our service mesh to handle cross-cutting concerns at the infrastructure level β€” service discovery, load balancing, mutual TLS, traffic splitting, and observability β€” without modifying application code.

Service Mesh Sidecar Pattern
Service A
β†’
envoy proxy
mTLS β†’
Service B
β†’
envoy proxy

Service Mesh Capabilities

  • mTLS: Automatic mutual TLS encryption between all services
  • Circuit Breaking: Automatic isolation of failing services
  • Canary Deployments: Traffic splitting for gradual rollouts
  • Rate Limiting: Global and per-service rate policies
  • Observability: Distributed tracing, metrics, and access logs

🚫 Anti-Patterns to Avoid

Anti-patterns are common responses to recurring problems that are ineffective and risky. Based on our production experience, avoid these:

1. Distributed Monolith

Services that are tightly coupled and must be deployed together, but are distributed across separate codebases. This gives you all the complexity of microservices with none of the benefits. Signal: if changing one service requires coordinated changes in three others, you have a distributed monolith.

2. Chatty Services

Services that require multiple round-trips to complete a simple operation. This adds latency, increases failure surface, and makes debugging extremely difficult. Solution: batch operations, use aggregation services, or reconsider boundaries.

3. Shared Database Anti-Pattern

Multiple services sharing the same database. This creates tight coupling at the data layer and prevents independent deployment. Rule: each service owns its own database. If data sharing is needed, use events or a materialized view service.

4. Over-Engineering

Applying complex patterns (CQRS, Event Sourcing, Saga) to problems that don't require them. YAGNI principle: start simple and add complexity only when you have measurable evidence that it's needed.

🚨

Golden Rule

Every architectural pattern adds complexity. The best architecture is the simplest one that satisfies your current and near-future requirements. Complexity should be introduced intentionally and justified by data.

🧭 Choosing the Right Pattern

Use this decision framework when evaluating architectural patterns for new services or refactoring existing ones:

Decision Flow

  1. Start with a modular monolith β€” apply layered architecture within bounded modules
  2. Extract to microservices only when: independent scaling is needed, different teams own the domain, or technology heterogeneity is required
  3. Add event-driven communication when: services need to react to state changes asynchronously, or when building real-time pipelines
  4. Apply CQRS when: read and write workloads have fundamentally different performance requirements, or when you need optimized read views
  5. Use Event Sourcing when: complete audit trails are required, or when you need to replay history for temporal queries
  6. Consider Hexagonal for any service where testability and framework independence are priorities
  7. Serverless for edge workloads β€” image processing, notifications, scheduled tasks, webhooks
modular-monolith ddd bounded-context separation-of-concerns testability scalability resilience observability domain-driven-design event-driven cqs event-sourcing serverless service-mesh
ℹ️

Architecture Decision Records

All significant architecture decisions at Aevum Encyclopedia are captured as Architecture Decision Records (ADRs). These documents explain the context, options considered, decision made, and consequences β€” ensuring traceability and shared understanding.