Book notes: Software Architecture: The Hard Parts
Book notes on "Software Architecture: The Hard Parts" by Neal Ford, Mark Richards, Pramod Sadalage and Zhamak Dehghani
These are my notes on Software Architecture: The Hard Parts by Neal Ford, Mark Richards, Pramod Sadalage and Zhamak Dehghani.
A book full trade-off tables. The dream of any MinMax RPG player.
Key Insights
- Timeless skill: How architects make decisions, and how to objectively weight trade-offs.
- SW architecture is in the service of data.
- Trade-off analysis:
- Find what parts are entangles together.
- Analyze how they are coupled to one another.
- Assess trade-offs by determining the impact of change on interdependent systems.
- Primary reasons for breaking applications apart is:
- Time to market (== agility == deployability + testability + maintainability)
- Scalability.
- Availability.
- Fault tolerance.
- Tactical forking: Copy the entire monolith, and delete the code not needed.
- Coupling is the most significant factor in determining the overall success and feasibility of breaking a monolith.
- Size components:
- Not too big, not too small == ~ 1-2 standard deviation from average.
- Service granularity disintegrators:
- Service Scope and Function:
- Subjective, so don’t use alone.
- Code volatility:
- If a component changes more frequently than others, consider to split it, so that deployment and testing scope is smaller.
- Service Scope and Function:
- Strive for fine-grained libraries.
- The service that performs writes owns the data.
- As workflow complexity goes up, the need for an orchestrator raises.
- Tightly coupling violates one of the aspirational goals fo microservices, hence prefer loose contracts and consider using consumer-driven contracts.
- Data Mesh:
- Like microservices but for analytical data.
- Often, a solution has many beneficial aspects, but lacks critical capabilities that prevent success.
- Generic solutions are rarely useful in real-world architectures without applying additional situation-specific context.
- Use concrete use cases.
TOC
- Chapter 1 - What Happens When There Are No “Best Practices”?
- Part I - Pulling Things Apart
- Part II - Putting Things Back Together
- Chapter 8 - Reuse Patterns
- Chapter 9 - Data Ownership and Distributed Transactions
- Chapter 10 - Distributed Data Access
- Chapter 11 - Managing Distributed Workflow
- Chapter 12 - Transactional Sagas
- Chapter 13 - Contracts
- Chapter 14 - Managing Analytical Data
- Chapter 15 - Build Your Own Trade-off Analysis
Chapter 1 - What Happens When There Are No “Best Practices”?
- Architects constantly face difficult problems that literally no one has faced before (due to context).
- Timeless skill: How architects make decisions, and how to objectively weight trade-offs.
- SW architecture is in the service of data.
Part I - Pulling Things Apart
Chapter 2 - Discerning Coupling in SW Architecture
- No best practice exist that can you apply to real-world complex system.
- Trade-off analysis:
- Find what parts are entangles together.
- Analyze how they are coupled to one another.
- Assess trade-offs by determining the impact of change on interdependent systems.
- Architecture quantum:
- Independent deployable.
- High functional cohesion.
- High static coupling.
- Synchronous dynamic coupling.
- Dynamic coupling dimensions:
- Communication: sync or async.
- Consistency: atomic or eventual.
- Coordination: orchestration or choreography.
Chapter 3 - Architectural Modularity
- SW architecture must constantly change and adapt.
- Primary reasons for breaking applications apart is:
- Time to market (== agility == deployability + testability + maintainability)
- Scalability.
- Availability.
- Fault tolerance.
Chapter 4 - Architectural Decomposition
- Is a codebase decomposable?
- Architect to decide. Tools:
- Component-based decomposition:
- Enable migration to service-oriented architecture.
- Tactical forking:
- When there is little internal structure.
- Copy the entire monolith, and delete the code not needed.
Chapter 5 - Component-Based Decomposition Patterns
- Initially, apply in order:
- Identify and size components:
- Not too big, not too small == ~ 1-2 standard deviation from average.
- Size == # statements.
- Too big –> split.
- Gather common domain components:
- To eliminate duplication.
- Shared domain logic, not infrastructure.
- Mostly a manual process.
- Flatten components:
- Component == leaf package/namespace.
- All code should be in a component.
- Move shared code to its own component.
- Determine component dependencies:
- Coupling is the most significant factor in determining the overall success and feasibility of breaking a monolith.
- Both afferent and efferent coupling.
- Consider refactor to reduce coupling (like splitting a component in two).
- Create component domains:
- Domains =~ service.
- Package structure:
ss .customer.billing .payments .MonthlyBilling
app.domain .subdomain.component.class
- Move components to appropriate domain.
- Create domain services:
- Service-based architecture.
- Don’t apply this pattern until all domains have been identified and refactored.
- Identify and size components:
Chapter 6 - Pulling Apart Operational Data
- Evaluate data disintegrators and data integrators.
- Disintegrators:
- Change control:
- How many services are impacted by a DB change?
- All must be deployed at the same time.
- Connection management:
- Are we reaching the max number of connections allowed by the DB?
- Are we running out of DB connections?
- Scalability:
- Data is partitioned.
- Fault tolerance:
- Avoid DB as a single point of failure.
- Increase Architecture Quantum.
- Need for different kinds of DBs.
- Change control:
- Integrators:
- Data relationships:
- Referential integrity.
- DB transactions.
- Data relationships:
- Steps to split a DB:
- Analyze DB and create data domains:
- Data domain: group of tables/triggers/stored procs related to one domain.
- Assign tabled to data domains:
- Move tables to data domain (== DB schema).
- Allow for cross-schema joins.
- Separate DB connections to data domains:
- Disallow cross-schema joins.
- Replace joins with service calls.
- Move schemas to separate DB servers.
- Switch over to independent DB servers:
- Clean up old DB, do not connect to it.
- Analyze DB and create data domains:
- Selecting DB type:
- Relational DB:
- When not to use:
- Graph structures with arbitrary depth.
- Horizontal scalability required.
- Availability (over consistency).
- Reactive stream APIs.
- When not to use:
- Key-Value DB:
- Key -> blob.
- Single index.
- Document DB:
- Key -> JSON/XML.
- Multiple indices.
- Column Family DBs.
- Graph DB:
- Beginners tend to add properties to relations, while seasoned add nodes and relations.
- New SQL DB:
- Scalability of NoSQL with Relational DB features like ACID.
- CockroachDB
- Cloud Native DB:
- Mix bag of DBs (including Datomic).
- Time-Series DB.
- Relational DB:
Support means “Programming language support, product maturity, SQ: support, community”.
Chapter 7 - Service Granularity
- Granularity == size == # of statements + # public interfaces.
- Granularity disintegrators:
- Service Scope and Function:
- Consider cohesion and size.
- Single responsibility principle.
- Subjective, so don’t use alone.
- Code volatility:
- If a component changes more frequently than others, consider to split it, so that deployment and testing scope is smaller.
- Scalability and Throughput:
- Some component needs to scale more than others.
- Fault tolerance:
- One component more likely to crash and affect other component.
- Security:
- Different security requirements per component.
- Extensibility:
- If a new functionality will be more appropriate as a new service.
- Service Scope and Function:
- Granularity integrators:
- DB transactions.
- Workflow and choreography:
- Too many inter-service callas:
- Less fault tolerant.
- Less performant.
- Too many inter-service callas:
- Shared code (business code, not infrastructure).
- Data relationships.
Part II - Putting Things Back Together
Chapter 8 - Reuse Patterns
- Code replication:
- Copy and paste between services.
- Use for simple static code that is unlikely to change.
- Shared library:
- Trade-off between dependency management and change control.
- In general, strive for fine-grained libraries.
- Use in homogeneous environments where shared code change is low to moderate.
- Shared service:
- Easier to deploy changes, but:
- Less performance, scalable, fault tolerant.
- Versioning can be more difficult.
- Good in:
- Polyglot environments.
- High number of changes in shared functionality.
- Easier to deploy changes, but:
- Sidecars and service mesh:
- Mostly for cross-cutting operational concerns.
- Reuse is derived via abstraction but operationalized by slow rate of change.
Chapter 9 - Data Ownership and Distributed Transactions
- In general, the service that performs writes owns the data.
- Scenarios:
- Simple ownership:
- Only one writer.
- Writer owns the data.
- Common ownership:
- Most services write to the table.
- Solution: create a new service that owns the data.
- Joint ownership:
- Solutions:
- Split table:
- May required data replication/synchronization between services on delete/create of primary entity.
- Data domain techniques:
- Both services own the table, so keep as it is.
- Delegate techniques:
- One service owns the data, the other makes service calls.
- Consider owner by primary domain or by operational characteristic.
- Service consolidation technique:
- Combine possible owners in a bigger service.
- Split table:
- Solutions:
- Simple ownership:
- Distribute transactions, eventual consistency patterns:
- Background synchronization pattern:
- Separate external process or service that periodically check data sources and keeps them in sync.
- Pro: no inter-service communication.
- Cons:
- Slow eventual consistency.
- High coupling between sync process and all other processes.
- Complex implementation.
- Orchestrated Request-Based Pattern:
- One process/service in charge of making synchronous requests to other services.
- Prefer a dedicated orchestrator.
- Pro:
- Services still decoupled, if orchestrator is standalone.
- Favours consistency over availability.
- Atomic business requests.
- Cons:
- Slower responsiveness.
- Complex error handling.
- Usually requires compensating transactions.
- Event-based pattern:
- Pro:
- Decoupled services.
- Fast data consistency.
- Fast responsiveness.
- Cons: complex error handling.
- Pro:
- Background synchronization pattern:
Chapter 10 - Distributed Data Access
- Data access patterns:
- Inter-service calls.
- Column schema replication:
- Keep local copy of the other service data.
- Duplicated caching pattern:
- Same as (2) but in-memory and using some product (Hazelcast, Ignite).
- Pro: data remains consistent and ownership is preserved.
- How much I disagree on this?
- Data domain pattern:
- Share DB, same as joint ownership.
Chapter 11 - Managing Distributed Workflow
- Orchestration communication style:
- Aka mediator.
- In microservices, one orchestrator per workflow.
- Responsibilities:
- Workflow state.
- Optional behaviour.
- Error handling.
- Notification.
- Choreography communication style:
- Workflow state management patterns:
- Front controller pattern:
- First called service owns the state.
- Other services may query and update the state.
- Stateless choreography:
- Query individual services to know the state of the workflow.
- Stamp coupling:
- Add workflow state in the message between services.
- Each service updates its part.
- Front controller pattern:
- Workflow state management patterns:
- As workflow complexity goes up, the need for an orchestrator raises.
Chapter 12 - Transactional Sagas
Types:
- Epic saga:
- “Traditional”.
- Avoid.
- Phone Tag:
- Like Epic but without a coordinator.
- Fairy Tale:
- As Epic but without distributed transactions.
- Fantasy Fiction:
- To improve the Epic saga performance, but it fails.
- Horror Story:
- Building atomicity on top of async + no mediator.
- Epic saga:
R/A stands for Responsiveness/Availability.
- S/E stands for Scale/Elasticity.
Saga | Communication | Consistency | Coordination | Coupling | Complexity | R/A | S/E |
---|---|---|---|---|---|---|---|
Epic | Sync | Atomic | Orchestrated | Very high | Low | Low | Very low |
Phone Tag | Sync | Atomic | Choreographed | High | High | Low | Low |
Fairy Tale | Sync | Eventual | Orchestrated | High | Very low | Medium | High |
Time Travel | Sync | Eventual | Choreographed | Medium | Low | Medium | High |
Fantasy Fiction | Async | Atomic | Orchestrated | High | High | Low | Low |
Horror Story | Async | Atomic | Choreographed | Medium | Very high | Low | Medium |
Parallel | Async | Eventual | Orchestrated | Low | Low | High | High |
Anthology | Async | Eventual | Choreographed | Very low | High | High | Very high |
- Consider state machines (instead of atomic distributed transactions) to know the current state of a transactional saga.
Chapter 13 - Contracts
- Anti-pattern: include in contract more information than needed.
- Strict contracts:
- Pros:
- Guaranteed contract fidelity.
- Versioned.
- Easier to verify at build time.
- Better documentation.
- Cons:
- Tight coupling.
- Versioned.
- Pros:
- Loose contracts:
- Pros:
- Highly decoupled.
- Easier to evolve.
- Cons:
- Contract management.
- Requires fitness functions.
- Pros:
- Tightly coupling violates one of the aspirational goals fo microservices, hence prefer loose contracts and consider using consumer-driven contracts.
Chapter 14 - Managing Analytical Data
- Data Warehouse:
- Transform data at ingestion.
- Brittle integration.
- Usually failed to deliver.
- Technical partition.
- Data Lake:
- Transform data at usage time.
- Difficult to discover proper assets.
- PII and sensitive info issues.
- Technical partitioning.
- Data Mesh:
- Like microservices but for analytical data.
- Principles:
- Domain ownership of data:
- Distributed and shared in a peer-to-peer fashion.
- Data as a product:
- Data product quantum:
- Adjacent and coupled to microservices.
- Always async communication.
- Data product quantum:
- Self-service platform:
- Oriented to sharing and consuming data.
- Computational federated governance:
- Policies automated and embedded as sidecars.
- Domain ownership of data:
Chapter 15 - Build Your Own Trade-off Analysis
- Often, a solution has many beneficial aspects, but lacks critical capabilities that prevent success.
- Generic solutions are rarely useful in real-world architectures without applying additional situation-specific context.
- Use concrete use cases.
- Reduce trade-off analysis to a few key points:
- Translate them to non-technical parlance for non-tech people.