Tuesday, November 25, 2025

Modern Architecture and Design Patterns




Note: all patterns are documented in a shortened version of the Pattern-Oriented Software Architecture pattern documentation template.


Introduction


In the rapidly evolving landscape of software development, architecture and design patterns continue to serve as the cornerstone for building robust, scalable, and maintainable systems. This catalog presents both time-tested patterns from the Gang of Four and modern architectural patterns that have emerged to address contemporary challenges in distributed systems, cloud-native applications, and event-driven architectures.

The patterns in this catalog are organized into several categories: Classical Design Patterns (Creational, Structural, Behavioral), Modern Architectural Patterns (Microservices, Event-Driven), Cloud-Native Patterns (Resilience, Scalability), and Integration Patterns (Messaging, Data Management).

Each pattern follows the established format of Context, Problem, Solution, and Consequences, providing practitioners with clear guidance on when and how to apply these patterns effectively.


Classical Design Patterns 


Factory Method Pattern

Context: Applications need to create objects without specifying their exact classes, particularly when the specific type of object to create is determined at runtime or varies based on configuration.

Problem: Direct instantiation of objects using the `new` operator creates tight coupling between the client code and concrete classes. This makes the system inflexible when new types need to be added or when object creation logic becomes complex. How can we create objects while maintaining loose coupling and extensibility?

Solution: Define an interface for creating objects, but let subclasses decide which class to instantiate. The Factory Method pattern encapsulates object creation in a method, allowing subclasses to override this method to change the class of objects that will be created.

Creator

├── factoryMethod() : Product

└── ConcreteCreator

    └── factoryMethod() : ConcreteProduct

Consequences:

Benefits: Eliminates the need to bind application-specific classes into code; provides flexibility in object creation; follows the Open-Closed Principle by allowing new products without changing existing code.

Liabilities: Requires creating a subclass for each concrete product; can lead to proliferation of classes in complex scenarios.


Observer Pattern

Context: An object (subject) maintains a list of dependents (observers) and needs to notify them automatically of state changes, typically by calling one of their methods.

Problem: Objects need to be notified of changes in other objects without creating tight coupling between them. Direct references would make the system rigid and difficult to maintain. How can we maintain loose coupling while ensuring proper notification of state changes?

Solution: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. The pattern involves a subject that maintains a list of observers and provides methods to add, remove, and notify them.

Consequences:

Benefits: Supports loose coupling between subject and observers; allows for dynamic relationships at runtime; follows the Single Responsibility Principle.

Liabilities: Can lead to memory leaks if observers aren’t properly removed; unexpected update chains can cause performance issues; debugging can be complex due to indirect relationships.


Adapter Pattern

Context: Existing classes need to work together but have incompatible interfaces, often occurring when integrating legacy systems with new components or third-party libraries.

Problem: You want to use an existing class, but its interface doesn’t match the one you need. Creating a new class from scratch would be wasteful when the functionality already exists in an incompatible form.

Solution: Create an adapter class that translates between the incompatible interfaces. The adapter implements the target interface and holds a reference to an instance of the adaptee, translating calls from the target interface to the adaptee’s interface.

Consequences:

Benefits: Allows classes with incompatible interfaces to work together; promotes code reuse; can add functionality while adapting.

Liabilities: Increases code complexity by introducing another layer; may impact performance due to additional method calls; adapter code needs maintenance when either interface changes.


Command Pattern

Context: Applications need to parameterize objects with operations, queue operations, log requests, and support undo functionality.

Problem: Sometimes you need to issue requests to objects without knowing anything about the operation being requested or the receiver of the request. How can we decouple the object that invokes the operation from the one that performs it?

Solution: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations. The command object holds all the information needed to execute the action.

Consequences:

Benefits: Decouples the object that invokes the operation from the receiver; allows for parameterization of clients with different requests; supports undo/redo operations; enables logging and queuing of requests.

Liabilities: Can lead to many small command classes; may complicate the design when simple callback functions would suffice.



Modern Architectural Patterns


Microservices Architecture Pattern

Context: Large, complex applications need to be developed and deployed by multiple teams with different technologies and release cycles. The application must be scalable, maintainable, and resilient to failures.

Problem: Monolithic applications become increasingly difficult to maintain, scale, and deploy as they grow. Changes require retesting and redeploying the entire application, limiting development velocity and technology choices. How can we decompose large applications into smaller, manageable, and independently deployable services?

Solution: Architect the application as a suite of small, independently deployable services. Each service runs in its own process, communicates via well-defined APIs (typically HTTP/REST or messaging), and is organized around business capabilities. Services can be developed by different teams using different technologies.

Consequences:

Benefits: Independent deployment and scaling; technology diversity; team autonomy; improved fault isolation; better alignment with business capabilities.

Liabilities: Increased complexity in service coordination; network latency and reliability issues; data consistency challenges; operational overhead; testing complexity across service boundaries.


Event-Driven Architecture Pattern

Context: Systems need to react to changes and events in real-time, often involving multiple components that must remain loosely coupled while maintaining consistency and responsiveness.

Problem: Traditional request-response patterns create tight coupling between components and don’t scale well for systems that need to handle many concurrent events or support complex workflows spanning multiple services. How can we build systems that react efficiently to events while maintaining loose coupling?

Solution: Use events as the primary means of communication between system components. Event producers publish events to a message broker or event bus, and event consumers subscribe to relevant events. This creates a publish-subscribe model where components communicate through events rather than direct calls.

Consequences:

Benefits: Loose coupling between components; high scalability and responsiveness; supports complex event processing; enables real-time reactions to changes.

Liabilities: Eventual consistency challenges; increased complexity in debugging and tracing; potential for event ordering issues; requires robust message broker infrastructure.


CQRS (Command Query Responsibility Segregation) Pattern

Context: Applications with complex read and write operations that have different performance, scalability, and consistency requirements need optimization for both command and query operations.

Problem: In traditional CRUD applications, the same data model is used for both read and write operations. This creates conflicts between the optimizations needed for different types of operations. Write operations often require different validation and business logic, while read operations need to be optimized for query performance and may require denormalized data.

Solution: Separate the read and write operations into different models. Commands (write operations) use one model optimized for writing and business logic, while queries (read operations) use a different model optimized for reading and reporting. These models can have different data stores, schemas, and scaling characteristics.

Consequences:

Benefits: Independent optimization of read and write operations; improved performance and scalability; better separation of concerns; supports eventual consistency models.

Liabilities: Increased complexity due to separate models; data synchronization challenges; potential for consistency issues; requires careful design of the synchronization mechanism.


Saga Pattern

Context: Microservices applications need to maintain data consistency across multiple services when executing business transactions that span service boundaries.

Problem: Distributed transactions using two-phase commit (2PC) are not viable in microservices architectures due to the availability and performance requirements. Services need to maintain their own databases, but business operations often require coordinated changes across multiple services. How can we maintain data consistency without distributed transactions?

Solution: Implement distributed transactions as a series of local transactions, where each transaction updates data within a single service. The saga coordinates these transactions through a sequence of events, and if any transaction fails, compensating transactions are executed to undo the changes made by previous transactions.

Consequences:

Benefits: Maintains consistency without distributed transactions; improves availability and performance; supports long-running business processes; provides clear failure handling.

Liabilities: Complexity in designing compensating transactions; eventual consistency model; requires careful ordering of operations; challenging to implement and debug.


Cloud-Native Resilience Patterns


Circuit Breaker Pattern

Context: Distributed systems with service dependencies need to handle failures gracefully and prevent cascading failures when remote services become unavailable or unresponsive.

Problem: When a service is failing or responding slowly, continuing to call it wastes precious resources such as network connections, threads, and memory. These resources might be consumed for the duration of the timeout period, and multiple callers can exhaust the resource pool, leading to cascading failures throughout the system.

Solution: Wrap calls to remote services with a circuit breaker object that monitors for failures. When failures reach a certain threshold, the circuit breaker opens and immediately returns an error without attempting to call the remote service. After a timeout period, the circuit breaker allows limited calls through to test if the service has recovered.

The pattern operates in three states:

  • Closed: Normal operation, calls pass through
  • Open: Calls fail immediately, service is considered down
  • Half-Open: Limited calls allowed to test service recovery

Consequences:

Benefits: Prevents resource exhaustion; provides fast failure detection; allows automatic recovery testing; improves system stability and user experience.

Liabilities: Requires careful configuration of thresholds and timeouts; adds complexity to service calls; may mask intermittent issues; requires monitoring and alerting.


Bulkhead Pattern

Context: Applications need to isolate different workloads or tenants to prevent resource exhaustion in one area from affecting the entire system.

Problem: In a shared resource environment, high demand or failures in one part of the system can consume all available resources, causing other parts of the system to fail. This can lead to cascading failures where one problematic component brings down the entire system.

Solution: Isolate elements of an application into pools so that if one fails, the others continue to function. Like the bulkheads of a ship that prevent a breach in one compartment from sinking the entire vessel, this pattern partitions resources to prevent total system failure.

Implementation approaches include:

  • Thread pool isolation for differentservices
  • Connection pool separation
  • Process or container isolation
  • Queue partitioning for different workload types

Consequences:

Benefits: Prevents cascading failures; enables independent scaling of different workloads; improves fault isolation; allows for different quality of service levels.

Liabilities: Increased resource overhead; complexity in resource management; potential resource underutilization; requires careful capacity planning.


Strangler Fig Pattern

Context: Legacy applications need to be modernized or migrated to new architectures without disrupting business operations or requiring a complete system rewrite.

Problem: Monolithic legacy systems are often too large and complex to replace all at once. A “big bang” migration approach is risky and disruptive to business operations. Additionally, the business cannot wait for a complete rewrite and needs new features to be delivered continuously.

Solution: Gradually replace specific pieces of functionality with new applications and services. Create a proxy or routing layer that intercepts requests and routes them to either the legacy system or the new system based on the functionality being accessed. Over time, more functionality is moved to the new system until the legacy system can be completely decommissioned.

The migration process involves:

  1. Identify components to migrate
  2. Create new services to replace legacy functionality
  3. Route traffic between old and new systems
  4. Gradually move more functionality
  5. Decommission legacy components

Consequences:

Benefits: Reduces migration risk; enables continuous feature development; allows incremental testing and validation; minimizes business disruption.

Liabilities: Requires access to legacy system code; increases system complexity during migration; requires careful coordination between old and new systems; longer migration timeline.


Retry Pattern

Context: Applications making calls to remote services or resources need to handle transient failures that may resolve themselves after a short period.

Problem: Network calls and remote service invocations can fail due to temporary issues such as network congestion, service overload, or brief connectivity problems. These transient failures often resolve quickly, but failing immediately reduces system reliability and user experience.

Solution: Automatically retry failed operations with appropriate delay strategies. Implement exponential backoff to avoid overwhelming the failing service and include jitter to prevent the “thundering herd” problem when multiple clients retry simultaneously.

Common retry strategies include:

  • Fixed interval: Constant delay between retries
  • Exponential backoff: Increasing delays (1s, 2s, 4s, 8s…)
  • Random jitter: Adding randomness to prevent synchronized retries

Consequences:

Benefits: Improves reliability against transient failures; simple to implement; handles temporary network issues well.

Liabilities: Can mask persistent problems; may increase load on failing services; requires careful configuration of retry counts and delays; can increase response times.


Event-Driven and Messaging Patterns


Event Sourcing Pattern

Context: Applications need to maintain a complete audit trail of changes, support temporal queries, or rebuild application state from historical data.

Problem: Traditional state-based persistence loses information about how the current state was reached. This makes it difficult to audit changes, debug issues, or implement complex business logic that depends on the sequence of events. Additionally, concurrent updates can lead to conflicting changes that are hard to resolve.

Solution: Store the sequence of events that led to the current state instead of storing just the current state. The application state is derived by replaying these events. Events are immutable and append-only, providing a complete audit trail and enabling powerful capabilities like temporal queries and event replay.

Key components:

  • Events: Immutable facts about what happened
  • Event Store: Database optimized for storing events
  • Event Handlers: Components that process events to update state
  • Projections: Read models derived from events

Consequences:

Benefits: Complete audit trail; supports temporal queries and replay; enables complex business logic based on event history; natural fit for event-driven architectures.

Liabilities: Complexity in querying current state; requires CQRS for efficient queries; event schema evolution challenges; storage overhead for historical data.


Publish-Subscribe Pattern

Context: Multiple subscribers need to be notified of events or messages from publishers without the publishers knowing about the specific subscribers.

Problem: In systems with many producers and consumers of information, direct communication would create tight coupling between components. Publishers would need to know about all their subscribers, making the system difficult to modify and extend.

Solution: Introduce an intermediary (message broker or event bus) that receives published messages and delivers them to all registered subscribers. Publishers send messages to topics or channels without knowing who will receive them, and subscribers express interest in specific types of messages.

Consequences:

Benefits: Loose coupling between publishers and subscribers; dynamic subscription management; supports one-to-many and many-to-many communication patterns.

Liabilities: Requires reliable message broker infrastructure; potential for message ordering issues; subscribers must handle duplicate messages; debugging can be complex due to indirect communication.


Message Router Pattern

Context: Messages need to be routed to different destinations based on their content, type, or other criteria in enterprise integration scenarios.

Problem: In complex integration scenarios, messages from a single source may need to be processed by different systems based on their content or metadata. Hard-coding routing logic makes the system inflexible and difficult to maintain.

Solution: Insert a Message Router component between the message source and destinations. The router examines incoming messages and determines the appropriate destination(s) based on configurable routing rules. The routing logic is separated from both the message producers and consumers.

Types of routing include:

  • Content-based routing: Based on message payload
  • Header-based routing: Based on message metadata
  • Context-based routing: Based on external conditions
  • Multicast routing: Sending to multiple destinations

Consequences:

Benefits: Flexible message routing; separation of routing concerns; supports complex integration patterns; configurable routing rules.

Liabilities: Single point of failure; potential performance bottleneck; increased complexity in message flow; requires robust error handling.


Data Management Patterns


Database per Service Pattern

Context: Microservices need to maintain loose coupling and independent deployment while managing their data persistence requirements.

Problem: Shared databases create coupling between services and can become bottlenecks for both development and runtime performance. Services sharing databases must coordinate schema changes and are vulnerable to failures in other services accessing the same database.

Solution: Each microservice maintains its own private database. Services can only access their own database and must use the service’s API to access data owned by other services. This ensures loose coupling and enables services to choose the most appropriate database technology for their needs.

Consequences:

Benefits: Loose coupling between services; independent data model evolution; technology diversity; better fault isolation.

Liabilities: Data consistency challenges across services; increased complexity in queries spanning multiple services; data duplication; requires careful transaction boundary design.


API Composition Pattern

Context: Queries need to retrieve data that spans multiple microservices, but services maintain separate databases following the Database per Service pattern.

Problem: Implementing queries that join data owned by multiple services becomes complex when each service has its own database. Traditional database joins are not possible across service boundaries, and there’s no single place to execute complex queries.

Solution: Implement a query by defining an API Composer that invokes the APIs of the services that own the data and performs an in-memory join of the results. The composer can be implemented as either a standalone service or as part of the client application.

Consequences:

Benefits: Simple to understand and implement; maintains service autonomy; works well for simple queries.

Liabilities: Increased latency due to multiple service calls; potential for inconsistent data if services are updated during query execution; limited support for complex queries; doesn’t scale well for large result sets.


Shared Data Pattern

Context: Multiple services need to access common reference data or shared configuration that changes infrequently and doesn’t have clear ownership boundaries.

Problem: Some data is naturally shared across services and doesn’t belong to any single service’s domain. Duplicating this data across services creates maintenance overhead and consistency problems, while accessing it through service APIs adds unnecessary complexity and latency.

Solution: Create a shared data store that multiple services can access directly for read-only reference data or configuration. This violates the strict Database per Service pattern but provides a pragmatic solution for truly shared, stable data.

Consequences:

Benefits: Reduces data duplication; improves query performance for shared data; simplifies access to reference data.

Liabilities: Creates coupling between services; shared schema evolution becomes complex; potential single point of failure; violates microservices principles.


Integration and Communication Patterns


API Gateway Pattern

Context: Client applications need to interact with multiple microservices, but direct communication creates complexity in client implementations and exposes internal service structure.

Problem: In a microservices architecture, clients would need to make multiple requests to different services, handle service discovery, deal with different protocols, and implement cross-cutting concerns like authentication and rate limiting in each client.

Solution: Implement a single entry point (API Gateway) that acts as a reverse proxy, routing requests to appropriate microservices. The gateway handles cross-cutting concerns like authentication, authorization, rate limiting, request/response transformation, and monitoring.

Consequences:

Benefits: Simplifies client implementations; centralized cross-cutting concerns; reduces the number of client-service network calls; provides a single point for monitoring and security.

Liabilities: Potential single point of failure; can become a bottleneck; risk of the gateway becoming a monolith; requires careful design to avoid coupling services to the gateway.


Backend for Frontend (BFF) Pattern

Context: Different client applications (mobile, web, desktop) have varying requirements for data formats, aggregation levels, and API contracts when consuming microservices.

Problem: A single API gateway or set of microservices may not efficiently serve all client types. Mobile clients might need highly optimized, minimal data sets, while web clients might require different data aggregations. Creating a one-size-fits-all API leads to over-fetching or under-fetching of data.

Solution: Create separate backend services tailored for each client or client type. Each BFF aggregates data from multiple microservices and presents it in the format most suitable for its specific frontend. This allows optimization for each client’s unique requirements.

Consequences:

Benefits: Optimized APIs for each client type; reduced over-fetching and under-fetching; ability to evolve client-specific functionality independently.

Liabilities: Code duplication across BFFs; increased operational complexity; potential for divergent client experiences; requires coordination between frontend and BFF teams.


Compensating Transaction Pattern

Context: Distributed systems need to maintain consistency across multiple services without using distributed transactions, particularly when operations cannot be easily rolled back.

Problem: In microservices architectures, operations often span multiple services, each managing their own data. If one operation in a sequence fails, previous operations need to be undone to maintain consistency, but traditional transaction rollback mechanisms don’t work across service boundaries.

Solution: Define compensating operations for each step in a distributed transaction. If any step fails, execute the compensating transactions for all completed steps in reverse order. Each compensating transaction should undo the effects of its corresponding forward transaction.

Consequences:

Benefits: Enables consistency in distributed systems; avoids the complexity and performance issues of distributed transactions; supports long-running business processes.

Liabilities: Increased complexity in designing compensating operations; some operations cannot be perfectly compensated; requires careful error handling and state management; eventual consistency model.


Conclusion


The patterns presented in this catalog represent a comprehensive toolkit for modern software architecture. Classical design patterns like Factory Method and Observer remain fundamental building blocks, while modern architectural patterns like Microservices and Event-Driven Architecture address contemporary challenges in distributed systems.

Cloud-native patterns such as Circuit Breaker and Bulkhead have become essential for building resilient systems that can handle failures gracefully. Event-driven and messaging patterns enable reactive architectures that can scale and adapt to changing demands.

The integration of these patterns requires careful consideration of their interactions and trade-offs. Successful architects understand not just individual patterns, but how they compose to create robust, scalable, and maintainable systems. As software architecture continues to evolve, these patterns provide a stable foundation for tackling new challenges while building on proven solutions.

The key to successful pattern application lies in understanding the context and problems each pattern addresses, carefully evaluating the consequences, and choosing the right combination of patterns for your specific architectural requirements.

No comments: