Software Architecture
Patterns
A structured reference covering foundational architecture patterns: Layers, Pipes & Filters, Microservices, and Dependency Injection.
Introduction to Patterns
Patterns allow us to reuse "thinking" β applying proven solutions to problems across different domains and levels of abstraction. They are discovered, not invented, and appear in software architecture, design, physical architecture, and many other fields.
Libraries
Allow us to reuse code β pre-built implementations that can be called from your own code.
Frameworks
Allow us to reuse structure β predefined application skeletons and control flows you fill in.
Patterns
Allow us to reuse thinking β applying solutions to problems across different domains and abstraction levels.
What Patterns Do
- Describe reusable (abstract or concrete) solutions for common problems
- Provide a recognizable structure and terminology for those solutions
Patterns Are Discovered, Not Invented
The universe is full of them. Patterns have been discovered in many fields:
- Software architecture
- Software design
- Analysis
- Physical (buildings) architecture
- Gardening, teaching, β¦
Designing With Patterns
Patterns offer approaches for:
- Decomposition of a system
- Distribution of responsibilities within a system
Patterns complement each other:
- Refinement and combination
Architectural β Buschmann [Bus+1996]
- Adaptable Systems
- Interactive Systems
- Mud-to-Structure
- Distributed Systems
Design Patterns β GoF [Gam+1994]
- Creational
- Structural
- Behavioral
Others: enterprise integration, enterprise architecture, microservices, concurrency, β¦
- Libraries reuse code; Frameworks reuse structure; Patterns reuse thinking
- Patterns describe reusable (abstract or concrete) solutions for common problems
- Patterns are discovered, not invented β the universe is full of them
- Patterns offer approaches for decomposition and distribution of responsibilities
- Patterns complement each other through refinement and combination
- CPSA-F must-know patterns: Layers, Pipes & Filters, Microservices, Dependency Injection
Layers Pattern
The Layers Pattern organizes components into stacked layers of services, where each layer encapsulates details, provides abstraction, and communicates with adjacent layers. Dependencies flow downward only β upper layers depend on lower layers, never the reverse.
π¦ Each Layer
- Encapsulates details
- May provide abstraction
- Provides services to the layer above
π Dependencies
- Can depend on layers below it
- Cannot depend on layers above it
π€ Usage
- Usage directed from upper to lower layer
β Communication
- Communication can be directed in both ways
| # | Layer | Responsibility |
|---|---|---|
| 7 | Application | High-level APIs |
| 6 | Presentation | "Data translator", encryption, compression |
| 5 | Session | Managing communication sessions |
| 4 | Transport | Reliable transmission of data |
| 3 | Network | Addressing, routing and traffic control |
| 2 | Data Link | Reliable transmission of data frames |
| 1 | Physical | RX/TX physical medium |
Example: ISO Open Systems Interconnection (OSI) model
Each layer can only interact with the layer directly below. No skipping.
Higher layers may call any layer below. Open layers can be bypassed.
Closed/Strict Layer
Advantages
- Modularity
- Simplifies debugging and testing
Disadvantages
- Potential inefficiency / performance overhead
Open Layer
Advantages
- Can bypass layers: higher efficiency / performance
Disadvantages
- Harder to understand/maintain
- Risk of bugs
- Layers are independent β distribution of tasks during development
- Layers are independent in production β installation and maintenance
- Implementations are interchangeable
- Unidirectional dependencies (no circular dependencies)
- The Layers pattern is easy to understand
- Layers are overhead when they only pass information to following layers
- Loose layering allows skipping layers (open) but increases dependency
- Some changes (e.g., adding a data field) may require changes to all layers
- Each layer encapsulates details and provides services to the layer above
- Dependencies are unidirectional: upper layers depend on lower, never circular
- Closed/Strict: each layer may only interact with the layer directly below
- Open: higher layers may call any layer below, bypassing intermediate layers
- Example: OSI 7-layer model, Web architecture (Presentation β Business β Persistence)
- Pro: independent layers, easy to understand. Con: overhead, changes may ripple
Pipes and Filters Pattern
The Pipes and Filters pattern decomposes a processing task into a sequence of independent processing steps (Filters) connected by channels (Pipes). Each Filter is unaware of others, transforming or manipulating data independently, enabling simple composition of complex pipelines.
Pipes
- Transport data/messages between filters
- Can buffer data
- Connect one filter's output to another's input
Filters
- Processing unit, unaware of other filters
- Transform, aggregate, or manipulate data
- Connects to multiple input/output pipes
Source
Sink
Programs/Processes connected by OS-allocated buffers as pipes
Raw Video
Output File
normalise format
aggregate Β· join
validate schema
Raw JSON from an API flows through 4 sequential filters before being stored. Each filter handles one concern only.
decode bytes
detect encoding
required fields
type coercion
normalise dates
currency convert
add metadata
join ref data
Deduplicate filter between Validate and Transform without touching the others.
- Simple (linear) dependencies
- Flexible β filters can be composed in many ways
- Easily scalable
- Filters can be developed independently
- Difficult error tracking and handling
- Buffers might overflow
- Sharing global data might be difficult
- Pipes transport data/messages between filters and can buffer data
- Filters are processing units, unaware of other filters
- Filters transform, aggregate, or manipulate data
- Filters connect to multiple input/output pipes
- Real examples: Unix pipes, GStreamer, ETL pipelines, video encoding
- Pro: flexible, scalable. Con: error tracking is hard, buffers may overflow
Microservices Architecture
Microservices architecture divides large systems into small, independently deployable units. Each microservice can be developed, deployed, scaled, and replaced independently, communicating over networks. It is ideal for large organizations with complex, big codebases.
As Codebase Grows, It Becomes Difficult To:
- Troubleshoot
- Add new features
- Build
- Test
- Load in IDE
Organizational Scalability Problems
- More engineers β more code merge conflicts
- Meetings become larger, longer, less productive
- Solution: consider migrating to Microservices
Divide large systems into small, independently operable units.
Each service: independently deployed Β· own database Β· communicates over network
Easier
- Understand
- Develop
Faster
- Build
- Test
- Load in IDE
Simpler
- Troubleshoot
- Add new features
β Polyglot Technology β each service picks its own language and data store independently.
β‘ Independent Scalability β scale only the service under load, not the entire system.
β must scale everything
β’ Higher Organisational Velocity β teams own, deploy, and operate their service independently (Inverse Conway Maneuver).
Single Responsibility Principle (SRP)
Each microservice owns one business capability. For example, in an online dating service: Image Service, User Profile Service, Matching Service, Billing Service β each routed through an API Gateway.
Separate Database Per Microservice
- Each service owns its data store independently
- Some data duplication (breaking DRY) is expected
- Some performance overhead of getting data from another microservice (Cost of Information Hiding)
Inverse Conway Law Maneuver: Organize teams around services (Subscription, Notification, Payments, Recommendations teams) so that team structure reflects the desired microservices architecture.
- Improve changeability and flexibility
- Decouple technology decisions (polyglot)
- Multiple versions of a service can coexist
- Good scalability β scale individual services
- Fast development, short time-to-market
- Higher organizational velocity β independent teams
- All CONs of distributed computing: network latency, outages, bandwidth limitations
- More sophisticated error handling required
- More complex deployment/operations
- Dependency resolution at runtime may cause hard-to-find errors
Microservices are perfect for large organizations with complex, big codebases β but we don't get all the benefits for free. We need to follow design principles: Single Responsibility and database per microservice.
- Divide large systems into small, independently operable units
- Each service can use a different technology stack (C#, Python, Java, Go)
- Services communicate over networks; separate database per service
- Benefits: smaller codebase, team autonomy, independent scaling
- Requires Single Responsibility Principle and database-per-service
- Con: distributed computing complexity, latency, error handling
Dependency Injection Pattern
Dependency Injection (DI) is the combination of Dependency Inversion Principle (DIP) and Inversion of Control (IoC). A dedicated Assembler component creates and injects concrete implementations into clients that depend on abstractions, decoupling high-level modules from low-level details.
Tight Coupling Problem
Without DI, PasswordManager directly depends on a concrete BcryptEncryptor:
PasswordManager pm = new PasswordManager(encryptor);
Nightmare for big classes β creating large sets of lower-level classes manually. Hard to swap implementations.
With DI β Abstract Interface
PasswordManager depends on the Encryptor interface, not a concrete class:
@Autowired
public PasswordManager(Encryptor enc) {
this.encryptor = enc;
}
The framework injects the correct concrete implementation at runtime.
PasswordManager
Encryptor
DiAssembler
DIP β Dependency Inversion Principle
- High-level modules should not depend on low-level modules
- Both should depend on abstractions
IoC β Inversion of Control
- Component registers itself with the framework, later called by the framework
- Third-party libraries define control flow
- Other applications: callbacks, scheduler, event loops, observer pattern
DI β Dependency Injection
- = Dependency Inversion Principle
- + Special application of IoC
- Creates and injects instances for abstractions a client depends on
app.encryptor.type=bcrypt or sha2) and uses reflection to inject the appropriate implementation at runtime. The application code never needs to know which concrete class is used.app.encryptor.type=bcrypt
String encrypt(String plainText);
}
public class BcryptEncryptor
implements Encryptor {
@Override
public String encrypt(String p) {
return BCrypt.hashpw(
p, BCrypt.gensalt());
}
}
public class SHA2Encryptor
implements Encryptor {
@Override
public String encrypt(String p) {
return DigestUtils
.sha256Hex(p);
}
}
public class PasswordManager {
private final Encryptor encryptor; // depends on abstraction, not impl
@Autowired
public PasswordManager(
@Qualifier("${app.encryptor.type}") Encryptor encryptor) {
this.encryptor = encryptor; // Spring injects at runtime
}
public String hashPassword(String password) {
return encryptor.encrypt(password); // calls Encryptor interface
}
}
- Supports the open-closed principle, loose coupling, and dependency inversion
- Manage dependencies "by configuration"
- Vastly improves extensibility
- Vastly improves flexibility and adaptability
- Vastly improves testability (mock components)
- Better readability of code
- Hard learning curve for developers
- Shifts complexity to DI frameworks
- Static analysis is difficult/impossible
- Things fail at run time (not compile time)
- DIP: high-level modules should not depend on low-level modules β both depend on abstractions
- IoC: third-party libraries define control flow rather than application-specific components
- DI = DIP + IoC: assembler creates and injects implementations at runtime
- Two injection styles: setter injection and constructor injection
- Pro: loose coupling, extensibility, testability (mock components)
- Con: hard learning curve, runtime failures, shifts complexity to DI framework
What & Why
- Proven, reusable solutions to recurring design problems
- Provide shared vocabulary for architects & developers
- Patterns vs. Practices β know the difference
Horizontal Separation
- Organize code into horizontal layers (presentation, business, data)
- Each layer depends only on the layer directly below
- Promotes SoC, testability & replaceability
Data Transformation Pipeline
- Filters = independent processing steps; Pipes = connectors
- Each filter reads, transforms, and writes data
- Easily add, remove or reorder processing steps
Independently Deployable Services
- Small, autonomous services organized around business capabilities
- Independent deployment, scaling & technology choices
- Trade-off: operational complexity & distributed data management
DIP + IoC in Action
- DIP β both high & low-level depend on abstractions
- IoC β framework controls flow; DI injects implementations
- Setter & constructor injection; great testability