Software Architecture Patterns
A comprehensive catalog of architectural patterns used across the Aevum Encyclopedia platform, with guidelines on when and how to apply each pattern in production systems.
ποΈ 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
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.
Layer Diagram
Example: Search Service Layered Structure
// 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
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.
Service Communication Flow
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 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 Flow: Article Published
Event Schema Example
{
"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 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.
Code Example: Command Handler
// 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 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.
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 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.
Event Stream for an Article
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 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.
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
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 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
- Start with a modular monolith β apply layered architecture within bounded modules
- Extract to microservices only when: independent scaling is needed, different teams own the domain, or technology heterogeneity is required
- Add event-driven communication when: services need to react to state changes asynchronously, or when building real-time pipelines
- Apply CQRS when: read and write workloads have fundamentally different performance requirements, or when you need optimized read views
- Use Event Sourcing when: complete audit trails are required, or when you need to replay history for temporal queries
- Consider Hexagonal for any service where testability and framework independence are priorities
- Serverless for edge workloads β image processing, notifications, scheduled tasks, webhooks
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.