Fundamentals of Software Architecture
Richards and Ford open with two laws that frame the entire book:
> First Law: Everything in software architecture is a trade-off. > > Second Law: Why is more important than how.
An architect's primary job is not to choose technologies — it is to analyze trade-offs and make decisions that align with business goals.
---
What Architects Do
Architecture is broader than code. An architect must:
The four dimensions of software architecture:
| Dimension | Concerns |
|---|---|
| Structure | Architecture style (microservices, layered, event-driven…) |
| Characteristics | Availability, scalability, security, agility… |
| Decisions | Rules and constraints the system must follow |
| Design principles | Guidelines (not hard rules) for implementation |
Architecture Characteristics
Also called "non-functional requirements" — but Richards and Ford reject that term. These are first-class architectural concerns, not afterthoughts.
Categories
Operational characteristics — runtime behavior:
Structural characteristics — internal quality:
Cross-cutting characteristics — apply everywhere:
The Least Worst Architecture
No architecture satisfies all characteristics simultaneously. Optimizing for one degrades others.
Scalability ←→ Simplicity (more nodes = more complexity)
Security ←→ Performance (encryption, validation add latency)
Agility ←→ Reliability (fast change = higher failure risk)
Deployability ←→ Testability (more deployments = more test surface)
> Identify the 3–7 characteristics that matter most for this system. Ignore the rest — designing for everything designs for nothing.
---
Modularity
The degree to which components of a system can be separated, recombined, and understood independently.
Three metrics to measure modularity:
Cohesion
How related are the elements inside a module?
High cohesion: OrderService handles only order operations
Low cohesion: UtilService handles orders, emails, logging, PDF generation
Ideal: functionally cohesive — all code in a module serves one purpose.
Coupling
How dependent are modules on each other?
A module with high afferent coupling is risky to change (many callers break)
A module with high efferent coupling is fragile (breaks if any dependency changes)
Connascence
A more precise measure of coupling — two components are connascent if a change in one requires a change in the other.
| Type | Example | Strength |
|---|---|---|
| Name | Method name used by caller | Weak |
| Type | Parameter type must match | Weak |
| Meaning | Magic number (0 = inactive) | Medium |
| Position | Argument order must match | Medium |
| Algorithm | Both sides must use same encoding | Strong |
| Execution order | Must call A before B | Strong |
---
Architecture Decisions
Architecture decisions define the constraints that structure how the system is built. They must be documented with rationale — not just the what, but the why.
Architecture Decision Records (ADRs)
A lightweight format to record decisions:
## ADR-001: Use PostgreSQL as primary data store
**Status:** Accepted
**Context:**
We need a relational store with ACID guarantees for financial transactions.
Team has existing PostgreSQL expertise.
**Decision:**
Use PostgreSQL as the primary data store.
**Consequences:**
- Strong consistency for transaction data
- Vertical scaling limits require sharding strategy at >10M records
- MySQL expertise from previous system is not transferable
Anti-Patterns in Architecture Decisions
Covering Your Assets — avoiding decisions to avoid blame. Architecture requires commitment.
Groundhog Day — re-litigating the same decisions repeatedly. ADRs prevent this.
Email-Driven Architecture — critical decisions buried in email threads. Use ADRs in version control instead.
---
Fitness Functions
Automated mechanisms to verify that the architecture remains consistent with its intended characteristics over time.
Architecture characteristic → Fitness function → Pass / Fail
Availability → uptime monitor, chaos injection tests
Modularity → ArchUnit test: no package cycles
Performance → load test: p99 latency < 200ms
Security → OWASP dependency scan in CI pipeline
Types:
> Fitness functions prevent architecture drift — the gap between the intended and actual architecture.
---
Component-Based Thinking
Architects think in components (deployable or logical units), not classes.
Component Identification Process
Partitioning Styles
Technical partitioning — organize by technical layer.
presentation/
├── UserController.java
service/
├── UserService.java
repository/
├── UserRepository.java
Cohesion is low across features — a "user" change touches all layers.
Domain partitioning — organize by business domain (DDD-inspired).
orders/
├── OrderController.java
├── OrderService.java
├── OrderRepository.java
users/
├── UserController.java
├── UserService.java
High cohesion per feature, lower coupling across domains. Preferred for microservices.
---
Architecture Styles
Layered (n-Tier)
The most common style. Separates concerns into horizontal layers.
Presentation Layer (UI, controllers)
↓
Business Layer (services, use cases)
↓
Persistence Layer (repositories, ORM)
↓
Database Layer
| Characteristic | Rating |
|---|---|
| Simplicity | High |
| Deployability | Low (deploy entire app) |
| Scalability | Low (scale entire app) |
| Testability | Medium |
| Elasticity | Low |
Pipeline
Data flows through a sequence of independent processing steps (pipes and filters).
Input → Filter A → Filter B → Filter C → Output
↑ ↑ ↑
(transform) (validate) (enrich)
Used by: Unix shell pipes, ETL pipelines, build tools, compilers.
| Characteristic | Rating |
|---|---|
| Modularity | High |
| Scalability | Medium |
| Simplicity | Medium |
| Deployability | Low (usually one unit) |
Microkernel (Plugin)
A stable core system with pluggable feature modules.
Core System (minimal, stable)
├── Plugin: PDF Export
├── Plugin: CSV Export
├── Plugin: Email Notification
└── Plugin: Slack Notification
Used by: IDEs (VS Code, IntelliJ), browsers (extensions), CMS platforms.
| Characteristic | Rating |
|---|---|
| Extensibility | High |
| Testability | High |
| Deployability | Medium |
| Scalability | Low |
Service-Based
A pragmatic distributed style: few coarse-grained services (4–12), all sharing a single database.
API Gateway
├── OrderService ┐
├── UserService ├── shared database
├── PaymentService ┘
└── NotificationService → own DB
Fewer operational concerns than microservices, more flexibility than monolith.
| Characteristic | Rating |
|---|---|
| Agility | High |
| Deployability | High |
| Scalability | Medium |
| Fault tolerance | Medium |
| Simplicity | Medium |
Event-Driven
Components communicate asynchronously via events. Two topologies:
Broker topology — events flow through a message broker, no central coordinator.
OrderPlaced event → broker
├── InventoryService (consumes)
├── BillingService (consumes) → PaymentProcessed event → broker
└── NotificationService (consumes)
Mediator topology — a central orchestrator coordinates the workflow.
OrderPlaced event → Mediator
→ calls InventoryService
→ calls BillingService
→ calls NotificationService
| Characteristic | Rating |
|---|---|
| Scalability | High |
| Fault tolerance | High |
| Agility | High |
| Simplicity | Low |
| Testability | Low |
Space-Based
Removes the database as the bottleneck. Uses in-memory data grids with replicated state across processing units.
Processing Unit 1 (in-memory cache + app logic)
Processing Unit 2 (in-memory cache + app logic)
Processing Unit N (in-memory cache + app logic)
↓
Virtualized middleware (routing, messaging, data replication)
↓
Database (async persistence — not in the hot path)
Used for: extreme high-concurrency systems (ticket sales, auction sites).
| Characteristic | Rating |
|---|---|
| Scalability | Very High |
| Elasticity | Very High |
| Performance | Very High |
| Simplicity | Very Low |
| Cost | Very High |
Microservices
Each service is independently deployable, owns its own data store, and communicates over network calls.
API Gateway
├── UserService → users DB
├── OrderService → orders DB
├── PaymentService → payments DB
└── SearchService → search index
Bounded context (from DDD): each service models one domain concept and is the single source of truth for it.
Sidecar pattern: cross-cutting concerns (logging, tracing, auth) extracted into a sidecar process — not embedded in business code.
Service Pod:
├── Business Service
└── Sidecar (logging, metrics, mTLS)
| Characteristic | Rating |
|---|---|
| Scalability | High |
| Agility | High |
| Deployability | High |
| Fault tolerance | High |
| Simplicity | Very Low |
| Data consistency | Low (eventual) |
| Cost | High |
Choosing an Architecture Style
There is no universally correct style. Choose based on the system's dominant characteristics.
Small team, CRUD app, time pressure → Layered
Plugin-extensible product → Microkernel
ETL, data processing pipeline → Pipeline
Moderate distribution, shared DB ok → Service-based
Async workflows, decoupled producers → Event-driven
Extreme throughput, in-memory scale → Space-based
Independent teams, deployability first → Microservices
Questions to ask before choosing:
---
Architecture Quantum
The smallest deployable unit with high functional cohesion and high architectural coupling.
Architecture quanta define the boundaries where different architecture characteristics can apply independently.
Order domain: high availability (active-active)
Report domain: low cost (single node, batch)
→ Different quanta → different deployment and scaling strategies
---
Diagramming Architecture
C4 Model (Context, Container, Component, Code)
Level 1 — System Context: your system + external users and systems
Level 2 — Container: applications, databases, queues inside your system
Level 3 — Component: services and modules inside a container
Level 4 — Code: classes, functions (usually auto-generated from IDE)
Use Level 1 and 2 for stakeholder communication. Level 3 for developer onboarding.
Rules for Architecture Diagrams
---
Key Takeaways
| Principle | Rule |
|---|---|
| Trade-offs are the job | There is no best architecture — only best for a context |
| Why over how | Record the rationale, not just the decision |
| Characteristics first | Identify the -ilities before picking a style |
| Fitness functions | Automate architectural governance or drift is inevitable |
| Modularity | Measure cohesion and coupling — don't guess |
| Domain partitioning | Prefer domain over technical layers for distributed systems |
| Quanta | Different parts of the system can have different architectures |
| ADRs | Every significant decision deserves a record |