Software Architecture Patterns

Published on May 17, 2025

This is an extracted notes from Software Architecture Patterns book, with some additional architectural style and real world example for the understanding.

An architectural style describes the macro structure of a system, while an architectural pattern is a reusable structural building block that can solve a particular problem within an architectural style. Design patterns, in contrast, impact how the source code is designed.

  • Architecture Pattern: Impacts the structural aspect of the system (e.g., CQRS).
  • Design Pattern: Impacts how the source code is designed (e.g., Builder, Template, Factory).

An architecture that lacks a clear vision or direction is often referred to as a “Big ball of mud”, characterized by being tightly coupled, brittle, and difficult to change.

CQRS (Command Query Responsibility Segregation)

CQRS is an architectural pattern that describes the structural separation between read (Query) and write (Command) operations to a database. This pattern can be applied to any architectural style.

When building distributed systems, it’s important to consider how different components interact. The principles from atomic design can be applied to system architecture as well.

Monolithic vs. Distributed Architecture

Choosing between a monolithic or distributed architecture depends on system requirements. A key question to ask is: “Does the entire system need to scale and support high availability, or only parts of the system?” For simple use cases, a monolithic architecture is often sufficient, while complex use cases requiring scalability might benefit from a distributed architecture.

Monolithic Architecture

Monolithic architectures are simpler and easier to design and implement.

ProsCons
CostScalability
SimplicityFault tolerance
Elasticity
Fatal errors, such as Out-of-memory errors can bring the whole system down.

Monolithic systems have a larger MTTR(Mean Time To Recovery ) and MTTS(Mean Time To Start). When a failure occurs, it takes a long time to recover. This long startup time negatively affects scalability and elasticity.

Examples of monolithic architecture styles include:

  • Layered architecture
  • Modular architecture
  • Pipeline architecture
  • Microkernel architecture
  • Hexagonal architecture

Distributed Architecture

Distributed architectures excel in scalability, elasticity, and fault tolerance. These characteristics come from the individual services.

ProsCons
ScalabilityComplexity from fallacies of distributed computing (e.g., the network is reliable, latency is zero, bandwidth is infinite).
ElasticityChallenges with distributed transactions.
Fault toleranceEventual data consistency.
AgilitySynchronization issues.
Smaller MTTR and MTTS than monolithicIncreased cost.
Fallacies of distributed computing

When designing a distributed system, architects and developers often make a set of false assumptions. These are known as the fallacies of distributed computing and ignoring them can lead to system failures and unexpected behavior.

  • The network is reliable: Networks are inherently unreliable. Connections can be lost, and packets can be dropped or corrupted.
  • Latency is zero: There is always a delay in sending data over a network. This latency can be significant and variable.
  • Bandwidth is infinite: Network bandwidth is a finite resource and can become a bottleneck.
  • The network is secure: A network can be compromised. Data must be protected through encryption and other security measures.
  • Topology doesn’t change: The network layout can change due to node failures, scaling events, or configuration updates.
  • There is one administrator: In a large system, multiple administrators or teams may be responsible for different parts of the network, leading to coordination challenges.
  • Transport cost is zero: There is a computational cost associated with serializing, transmitting, and deserializing data over a network.
  • The network is homogeneous: A network may consist of hardware and software from different vendors, with varying performance characteristics.
  • Versioning is simple: Managing different versions of services and ensuring backward compatibility is a complex challenge.
  • Compensating updates always work: Rolling back transactions that span multiple services (compensating transactions) is difficult to implement correctly and may not always succeed.
  • Observability is optional: Without proper monitoring, logging, and tracing, diagnosing and fixing problems in a distributed system is nearly impossible.

Source: https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing

Examples of distributed architecture styles include:

  • Event-driven architecture
  • Microservice architecture
  • Service-oriented architecture
  • Space-based architecture

Architecture Partitioning

The choice between technical and domain partitioning should be decided by the team structure and the nature of expected changes.

Technical Partitioning

Components are organized by their technical usage. The layered architecture is a prime example, where components are arranged into a presentation layer, business layer, and persistence layer. While this encapsulates logic, adding a new requirement might necessitate changes across multiple layers.

Examples of architectures that use technical partitioning:

  • Layered architecture
  • Microkernel architecture
  • Pipeline architecture
  • Event-driven architecture
  • Space-based architecture

Domain Partitioning

Components are organized by domain areas (e.g., User, Order, Shipping). Changes are often self-contained within a smaller radius, affecting only a particular domain or sub-domain.

Examples of architectures that use domain partitioning:

  • Microkernel architecture
  • Microservice architecture
  • Modular architecture
  • Service-based architecture

Microkernel architecture can be organised in both Technical and Domain Partitioning.

Architectural Styles

1. Layered Architecture

Also known as n-tier architecture, this style organizes components into horizontal layers, with each layer performing a specific technical role (e.g., presentation, business, persistence, database). A powerful feature of this style is the separation of concerns among components.

Layers of isolation mean that changes made in one layer generally do not impact or affect components in other layers. Layers are independent and have little to no knowledge of the inner workings of other layers.

In a closed layered architecture, a request must pass through the layers in order. In an architecture with open layers, a request can bypass some layers and access lower layers directly.

Layered architecture pattern diagram showing presentation, business, services, and persistence layers with open/closed layer access

The Services Layer is marked as OPEN, allowing the Business Layer to bypass it and directly access the Persistence Layer.

Anti-Pattern: The architecture sinkhole anti-pattern describes a situation where requests flow through multiple layers with little or no logic performed within each layer, acting as simple pass-throughs.

To avoid the sinkhole anti-pattern, it is recommended to apply an open layer to any layer where the majority of requests simply pass through without any processing logic.

2. Microkernel Architecture

Also known as a plug-in architecture, this style allows developers to add functionality as extensions or plug-ins to a core system without impacting its core functions. It consists of two main components: a core system and plug-in modules. The core system maintains a registry of available plug-ins.

Examples include IDEs, web browsers, and insurance processing systems where rules for different states act as plug-ins. A major drawback is that a fault in the core system will have a broad impact and can act as a bottleneck.

Microkernel architecture pattern diagram showing core system with plug-in modules for extensible functionality

3. Event-Driven Architecture

This architecture relies on highly decoupled, asynchronous event processors that trigger and respond to events in the system.

  • Event vs. Message:
    • An event is a notification about a state change (e.g., “I just placed an order”), with no knowledge of which services will respond.
    • A message is a command or request directed to a specific service (e.g., “Apply a payment to this order”). Event-driven systems typically use a publish-subscribe model, whereas message-driven systems use queues.

Due to its asynchronous and decoupled nature, this style excels in fault tolerance, scalability, and performance. It also offers excellent extensibility. However, it should not be used for systems that require synchronous processing, strong data consistency, or strict control over workflows and timing.

Event-driven architecture pattern diagram showing asynchronous event processors and message flow

4. Microservice Architecture

This architectural style is an ecosystem of single-purpose, separately deployed services. These services are typically accessed through an API Gateway.

A key concept is bounded context, which means all source code and data structures for a specific domain or subdomain are encapsulated as one unit. Only the service owning the data can access its database table directly; other services must go through the owning service’s API. This isolation helps manage change control within the ecosystem.

Microservices generally have low MTTR and MTTS. However, they introduce communication overheads, including network, security, and data latency.

Microservice architecture pattern diagram showing independently deployable services communicating through APIs

5. Space-Based Architecture

This style addresses the limitations of application scaling by removing the database from the direct transactional path. The database is often the ultimate bottleneck in highly scalable systems. The architecture uses a virtualized middleware with processing units that contain an in-memory data grid and a data replication engine. Data is read into the in-memory grids and asynchronously written back to the database, keeping the high-traffic transactional processing away from the database bottleneck.

Space-based architecture pattern diagram showing virtualized middleware with in-memory data grids bypassing database bottlenecks

6. Hexagonal Architecture

Also known as the Ports and Adapters architecture, this style aims to create loosely coupled application components that can be easily connected to their software environment. It achieves this by isolating the core application logic from outside concerns. The “inside” of the application communicates with the “outside” through a set of “ports,” which are interfaces defining the interaction. “Adapters” implement these interfaces and act as the bridge between the application core and external systems like databases, UIs, or third-party APIs. This makes the application independent of external technologies and easier to test in isolation.

Hexagonal architecture pattern diagram showing ports and adapters isolating core application logic from external dependencies

The core application logic defines ports, and various adapters (for UI, database, APIs, testing) implement these ports to interact with the core.

7. Pipes and Filters architecture

This architecture processes a stream of data in a series of stages. Each stage is a “filter” that performs a specific, isolated task on the data. The “pipes” are the channels that pass the data from one filter to the next. The filters are independent and can be reused and rearranged to create different processing pipelines. This pattern is common in data processing applications, compilers, and shell commands in UNIX-like operating systems (e.g., cat file.txt | grep "word" | wc -l). The key benefits are simplicity, reusability, and concurrency, as filters can often run in parallel.

Pipes and filters architecture pattern diagram showing data transformation pipeline with filters connected by pipes

Data flows from a source, through a series of pipes and filters where it is transformed, and finally to a data sink.

8. Service-Oriented Architecture (SOA)

SOA is an architectural style that promotes the use of loosely coupled, discoverable, and reusable services as the fundamental units of application development. Unlike microservices, which are typically fine-grained and independently deployable, SOA services are often larger, enterprise-level components. Communication between services is standardized, often through an Enterprise Service Bus (ESB), which provides routing, message transformation, and protocol conversion. The core principles of SOA are to increase agility and reduce IT costs by promoting the reuse of business services across an organization.

Service-oriented architecture (SOA) pattern diagram showing enterprise services communicating through ESB

Various applications consume services by communicating through a central Enterprise Service Bus (ESB), which routes requests to the appropriate enterprise service.

9. Modular Architecture

A modular architecture is a form of monolithic architecture that is structured into independent, interchangeable modules. Each module is self-contained and encapsulates a specific piece of business functionality, including its own business logic, data access, and UI components. While the application is deployed as a single unit (a monolith), the internal structure is highly organized, which improves maintainability, testability, and allows for parallel development. It’s often seen as a stepping stone between a traditional layered monolith and a microservices architecture.

Modular architecture pattern diagram showing self-contained business modules within a single deployment unit

The application is a single deployment unit with a shared database, but it is internally partitioned into distinct business domain modules that can communicate with each other.

Evolution of a Retail Application

This section illustrates how a retail application’s architecture might evolve from a simple monolith to a distributed system as its requirements grow in complexity.

Phase 1: The Simple Monolith

For a new retail application, the initial requirements are straightforward.

  • Initial Requirements:
    • Show a list of items.
      • Allow a customer to order an item.

At this stage, a Layered Monolithic Architecture is the most practical choice. It’s simple to develop, deploy, and manage, making it cost-effective for a startup.

Simple monolithic retail application architecture with basic presentation, business, and data layers

A single, unified application handles all functionality with distinct logical layers.

Phase 2: The Push to Modular Architecture

As the business grows, new requirements emerge that strain the simple monolithic structure.

  • Additional Requirements:
    • Team Growth: Separate teams are formed to handle different aspects of the business (e.g., a catalog team, an orders team, a customer management team).
    • Feature Complexity: A new promotions engine is needed to manage discounts and special offers, adding significant complexity.
    • Maintainability Issues: The single codebase becomes difficult to manage, and changes in one part of the application risk breaking another.
Modular retail application architecture showing separation into catalog, orders, customers, and promotions modules

These pressures lead to the adoption of a Modular Monolithic Architecture. The application is still a single deployable unit, but it is internally organized into distinct modules. This improves code organization, allows teams to work more independently, and makes the system easier to maintain.

Phase 3: The Move to Distributed Systems (Microservices)

The application becomes highly successful, leading to a new set of challenges that a monolith, even a modular one, cannot efficiently handle.

  • Additional Requirements Pushing to Distributed:
    • Scalability Demands: The product catalog is viewed millions of times a day, while the order processing volume is much lower. The entire application must be scaled to handle the peak traffic of its busiest component.
    • High Availability: A failure in the less-critical recommendations feature should not bring down the entire order processing system.
    • Technology Diversification: The data science team wants to build a new recommendation engine using Python and a machine learning framework, but the main application is written in Java.
    • Multiple Client Types: A new mobile app requires a dedicated API, which needs to evolve at a different pace than the web UI.

These requirements justify the significant investment of moving to a Microservices Architecture. Each business capability (catalog, orders, customers, recommendations) is broken out into a separate, independently deployable service. This allows each service to be scaled, updated, and even rewritten in a different technology stack without affecting the others.

Microservice retail application architecture with independent services for catalog, orders, customers, and recommendations

Phase 4: Enhancing with Event-Driven Communication

With multiple independent services, coordinating workflows and ensuring data consistency becomes a new challenge.

  • Additional Requirements Pushing to Event-Driven:
    • Complex Workflows: When an order is placed, multiple actions need to occur: payment must be processed, inventory updated, shipping arranged, notifications sent, and data sent to the analytics warehouse. Tightly coupling the Order Service to all these other services would create a brittle system.
    • Real-time Needs: Inventory levels must be updated in near real-time across the system to prevent overselling.
    • Resilience: If the notification service is temporarily down, it shouldn’t prevent new orders from being accepted.

To solve this, the architecture is enhanced with Event-Driven patterns. Instead of services calling each other directly, they publish events to a message broker (like RabbitMQ or Kafka). Other services subscribe to these events and react accordingly. This decouples the services, improves fault tolerance, and creates a more resilient and scalable system.

Event-driven retail application architecture showing microservices communicating through event broker for decoupled workflows

Even though the above diagram supports the scalability and reliability of the system, it also comes with increased operational complexity, such as more difficult debugging. Additionally, an event-driven system requires much greater attention to observability.