peopleanalyst

library / libbb3c5cfb137ad9bd

Software Architecture: The Hard Parts

Neal Ford, Mark Richards, Pramod Sadalage & Zhamak Dehghani · 2021

In a sentence

A rigorous, trade-off-driven guide to the most difficult structural, data, and communication decisions architects face when designing and evolving modern distributed systems.

Software Architecture: The Hard Parts equips software architects with a systematic framework for navigating the genuinely difficult decisions in distributed architectures—problems that have no universally correct answers, only competing sets of trade-offs. Using a single running example (the Sysops Squad ticketing system), the book walks through every major challenge: how to decompose a monolith into services using component-based patterns, how to pull apart and reassign ownership of operational data, how to choose the right database type, how to design service communication and coordination using eight named transactional saga patterns, how to manage distributed workflows via orchestration and choreography, how to handle code reuse without dangerous coupling, and how to separate analytical from operational data using the emerging data mesh pattern. Throughout, authors Neal Ford, Mark Richards, Pramod Sadalage, and Zhamak Dehghani demonstrate how to document decisions via Architecture Decision Records, automate governance via fitness functions, and—most importantly—build the skill of trade-off analysis so that architects can tackle novel problems their organizations have never faced before.

The four lenses

  • Science
  • Statistics
  • Systems
  • Strategy

Tags

f1-systems

The model

A causal-structural model describing how architectural design levers and contextual conditions determine coupling patterns, which in turn drive psychological and behavioral states (architect decision quality, team coordination) and ultimately produce system-level and organizational outcomes such as scalability, fault tolerance, maintainability, and business agility.

Decomposition Approach Qualitydesign lever

The degree to which the method chosen to break apart a monolithic codebase (component-based decomposition versus tactical forking) is appropriate given the internal structure of the codebase, resulting in well-formed, correctly sized, and logically grouped components that can become services.

Service Granularity Balancedesign lever

The degree to which the size and scope of each deployed service reflects an optimal equilibrium between disintegration forces (scope, volatility, scalability, fault tolerance, security, extensibility) and integration forces (database transactions, workflow, shared code, data relationships), producing services that are neither too fine-grained nor too coarse-grained.

Data Ownership Claritydesign lever

The extent to which every database table is assigned unambiguous ownership to a single service or well-defined data domain, eliminating joint and common ownership ambiguity, and enforcing bounded context rules that prevent unauthorized cross-schema access.

Dynamic Coupling Intensitypsychological state

The overall tightness of runtime coupling between services as determined by the combination of communication style (synchronous vs. asynchronous), consistency requirement (atomic vs. eventual), and coordination pattern (orchestrated vs. choreographed), corresponding directly to the eight saga pattern positions in the coupling matrix.

Static Coupling Scopedesign lever

The breadth of operational dependencies required to bootstrap a given architecture quantum, including OS, frameworks, transitive library dependencies, databases, message brokers, and other infrastructure components, determining whether the quantum can be independently deployed and operated.

Code Reuse Pattern Appropriatenessdesign lever

The degree to which the chosen mechanism for sharing common functionality (code replication, shared library, shared service, or sidecar/service mesh) is matched to the volatility of the shared code, the heterogeneity of the environment, and the acceptable coupling trade-offs, minimizing unnecessary runtime coupling while avoiding harmful duplication.

Contract Coupling Tightnessdesign lever

The degree of strictness embedded in the contracts used between services, ranging from tightly typed RPC-style schemas to loosely coupled name-value pairs, determining how much a change in one service's contract forces changes in its consumers and how easily the architecture can evolve.

Workflow Semantic Complexitycontextual condition

The inherent complexity of the business workflow being modeled in the distributed system, determined by the number of participating services, the number of alternate and error paths, and the degree to which compensating actions are required upon failure—a contextual condition that modifies the relative utility of orchestration versus choreography.

Data Volatilitycontextual condition

The rate at which data changes in a given domain or table, influencing decisions about which distributed data access pattern (interservice communication, column schema replication, replicated caching, or data domain) is feasible, and whether shared libraries or shared services are appropriate for domain logic.

Team Engineering Maturitycontextual condition

The degree to which development and architecture teams consistently apply engineering practices such as continuous integration, automated testing, fitness function governance, consumer-driven contract testing, and architectural decision documentation, enabling the full benefit of loose contracts and automated governance to be realized.

Architecture Quantum Countbehavioral pattern

The number of independently deployable units with high functional cohesion, high static coupling, and synchronous dynamic coupling present in the system, reflecting the degree to which the architecture achieves genuine deployment independence and the ability to assign distinct operational characteristics to each quantum.

Operational Architecture Characteristicsoutcome metric

The set of measurable non-functional qualities of the running system—including scalability, elasticity, fault tolerance, performance/responsiveness, and availability—that determine whether the architecture meets the operational needs of the business and are differentially achievable across architecture quanta with distinct characteristics.

Maintainability, Testability, and Deployabilityoutcome metric

The aggregate ease with which the system can be changed (maintainability), tested at appropriate scope (testability), and deployed frequently with low risk (deployability), forming the technical substrate of business agility and speed-to-market, improved by architectural modularity and harmed by excessive coupling.

Data Consistency Qualityoutcome metric

The degree to which data across distributed services remains correct, synchronized, and free from integrity violations over time, determined by the consistency model chosen (atomic versus eventual), the eventual consistency pattern implemented, and the effectiveness of error handling and compensating logic in the saga implementation.

Fitness Function Governance Coveragedesign lever

The proportion of critical architectural principles and constraints that are codified as automated, continuously running fitness functions in the CI/CD pipeline, preventing architectural drift and ensuring that important but non-urgent constraints are not inadvertently violated by development teams under schedule pressure.

How they connect

  • decomposition approach influences static coupling scope
  • decomposition approach influences architecture quantum count
  • service granularity balance influences dynamic coupling intensity
  • service granularity balance influences operational architecture characteristics
  • data ownership clarity influences data consistency quality
  • data ownership clarity influences dynamic coupling intensity
  • static coupling scope influences architecture quantum count
  • dynamic coupling intensity influences operational architecture characteristics
  • dynamic coupling intensity influences data consistency quality
  • architecture quantum count influences operational architecture characteristics
  • code reuse pattern appropriateness influences dynamic coupling intensity
  • code reuse pattern appropriateness influences maintainability testability deployability
  • contract coupling tightness influences dynamic coupling intensity
  • contract coupling tightness influences maintainability testability deployability
  • workflow complexity moderates dynamic coupling intensity
  • data volatility moderates code reuse pattern appropriateness
  • team engineering maturity moderates contract coupling tightness
  • fitness function coverage influences maintainability testability deployability
  • fitness function coverage influences static coupling scope
  • decomposition approach influences maintainability testability deployability
  • data ownership clarity influences operational architecture characteristics
  • architecture quantum count influences maintainability testability deployability

The process

This playbook outlines a comprehensive methodology for migrating a monolithic application to a distributed, modular architecture. The journey begins by establishing a clear business case for the migration, ensuring stakeholder buy-in. It then proceeds through a structured analysis of the existing system, covering architectural trade-offs, code decomposition strategies, and database separation. Key decisions are documented using Architectural Decision Records (ADRs) to maintain clarity and alignment. Once the monolith is broken down into services, the playbook addresses the core challenges of a distributed environment. This includes establishing clear data ownership, defining appropriate service granularity, and managing inter-service communication, code reuse, and complex business workflows. The process emphasizes a pattern-based approach, offering concrete techniques for handling data access, eventual consistency, and workflow orchestration or choreography. Finally, the playbook incorporates modern governance and data management principles. It advocates for the use of automated fitness functions to enforce architectural rules and introduces the Data Mesh framework for managing analytical data in a decentralized, domain-oriented fashion. The overall process is iterative and analytical, focusing on making informed, trade-off-aware decisions at each stage of the architectural evolution.

Build Business Case for Architectural Modularity

To justify the migration from a monolithic application to a distributed architecture by aligning technical challenges with business benefits.

When to use: When an existing monolithic system's limitations are impacting business goals, and a significant architectural change is being considered.

  1. Step 1Identify and document the existing issues with the monolithic application, such as poor maintainability, scalability, and availability.

    Entry: There are recognized problems with the current monolithic system.

    Exit: A comprehensive list of issues is documented.

    In: System performance data, Developer and user feedback · Out: Documentation of current system issues

    ch03

  2. Step 2Match the identified issues with the corresponding benefits of architectural modularity (e.g., improved agility, testability, deployability).

    Entry: System issues and modularity benefits are understood.

    Exit: A clear mapping between problems and solutions is created.

    In: Documentation of current system issues, Information on benefits of architectural modularity · Out: Problem-to-benefit mapping

    ch03

  3. Step 3Analyze and document the trade-offs involved in migrating from the monolith.

    Entry: The potential migration path is being considered.

    Exit: A list of trade-offs is documented.

    Out: Trade-off analysis document

    ch03

  4. Step 4Create a formal business case presentation and an Architecture Decision Record (ADR) outlining the decision to migrate.

    Entry: Problem-benefit mapping and trade-off analysis are complete.

    Exit: A business case and ADR are drafted.

    In: Problem-to-benefit mapping, Trade-off analysis · Out: Business case presentation, Architecture Decision Record (ADR)

    ch03

  5. Step 5Present the business case to stakeholders to seek approval and funding for the migration.

    Entry: Business case and ADR are ready for review.

    Exit: A decision on whether to proceed with the migration is made by stakeholders.

    • Approve or reject the migration project.

    In: Business case presentation · Out: Stakeholder approval

    ch03

Architectural Trade-off Analysis

To systematically identify, visualize, and evaluate architectural coupling and other trade-offs to make informed design decisions.

When to use: During the architectural design or refactoring phase to understand the implications of different design choices.

  1. Step 1Identify and discover entangled architectural dimensions and coupling points through collaborative discussion with experienced team members.

    Entry: An architectural decision needs to be made.

    Exit: A list of key coupling points and architectural dimensions is created.

    In: Knowledge of the existing architecture, Team expertise · Out: List of entangled dimensions and coupling points

    ch02 · ch17

  2. Step 2Create static and dynamic coupling diagrams to visualize dependencies.

    Entry: Coupling points have been identified.

    Exit: Visual diagrams representing system coupling are complete.

    In: Dependency information, Communication protocols · Out: Static coupling diagram, Dynamic coupling analysis

    ch02 · ch17

  3. Step 3Model various design combinations and assess the trade-offs for the identified coupling points.

    Entry: Coupling is visualized and understood.

    Exit: Viable design combinations and their trade-offs are documented.

    • Which design combinations are plausible and worth further analysis?

    In: List of coupling points, Potential design patterns · Out: Analysis of viable design combinations

    ch17

  4. Step 4Make an informed architectural decision based on the trade-off analysis.

    Entry: Trade-offs for viable designs are understood.

    Exit: A final architectural decision is made.

    • Which architectural approach best balances the competing concerns?

    In: Trade-off analysis document · Out: Architectural decision

    ch02 · ch17

Monolith Decomposition Strategy and Execution

To analyze a monolithic codebase and execute a structured decomposition into smaller, independent components or services.

When to use: After a decision has been made to break apart a monolith, this process guides the technical analysis and execution.

  1. Step 1Analyze the internal structure of the codebase to evaluate if it is decomposable.

    Entry: A decision to decompose the monolith has been made.

    Exit: An understanding of the codebase's structural integrity is achieved.

    • Is the codebase structured enough for component-based decomposition, or is it too chaotic, requiring tactical forking?

    In: Source code of the monolithic application, Static code analysis tools · Out: Codebase structure analysis report, Decision on decomposition strategy

    ch04

  2. Step 2Execute the chosen decomposition strategy: either Tactical Forking or Component-Based Decomposition.

    Entry: A decomposition strategy has been selected.

    Exit: The monolithic application is broken apart according to the chosen strategy.

    In: Source code, Chosen decomposition strategy · Out: A set of derived services or components

    ch04

  3. Step 3If using Component-Based Decomposition, systematically identify, analyze, and refine components.

    Entry: Component-Based Decomposition was chosen as the strategy.

    Exit: A well-defined and analyzed catalog of components is created.

    In: Source code, Static analysis tools · Out: Component catalog, Dependency diagram

    ch05

  4. Step 4Group the refined components into logical domain services.

    Entry: Components have been identified and analyzed.

    Exit: Components are organized into logical, self-contained domain services.

    • Which components logically form a cohesive functional domain?

    In: Component catalog, Dependency analysis · Out: Logical domain services, Refined component architecture

    ch05 · ch06

Database Decomposition

To break apart a monolithic database into smaller, independent databases aligned with data domains to improve scalability, resilience, and change management.

When to use: During a monolith-to-microservices migration, in parallel with code decomposition.

  1. Step 1Analyze the database schema to identify and create logical data domains.

    Entry: A decision to decompose the database has been made.

    Exit: A clear mapping of tables to data domains is created.

    • Determining which tables belong to which data domains.

    In: Existing monolithic database schema, Understanding of data domains · Out: Data domain definitions

    ch07

  2. Step 2Assign tables to their respective data domains, typically by moving them into separate schemas.

    Entry: Data domains are defined.

    Exit: Tables are logically grouped into new schemas.

    In: Data domain definitions · Out: Database schemas per data domain

    ch07

  3. Step 3Refactor the application's database connection logic to connect to the specific new schemas.

    Entry: New schemas have been created.

    Exit: Application services connect to the correct domain-specific schemas.

    In: Application source code · Out: Updated database connection logic

    ch07

  4. Step 4Physically move the schemas to separate database servers.

    Entry: Application is connecting to the new schemas on the monolithic server.

    Exit: Each schema now resides on its own independent database server.

    • Choose between backup/restore or replication for the physical move.

    In: Database schemas · Out: Independent database servers per data domain

    ch07

  5. Step 5Switch over to the independent database servers and remove connections to the old monolithic database.

    Entry: New database servers are live and tested.

    Exit: The monolithic database is no longer in use for these domains.

    ch07

Managing Data Ownership in a Distributed Architecture

To establish clear ownership of data tables for each service in a distributed system, preventing conflicts and ensuring data integrity.

When to use: After decomposing a database, to formally assign responsibility for data to the newly created services.

  1. Step 1For each database table, identify which services perform write operations.

    Entry: Database has been decomposed into domains/schemas.

    Exit: A mapping of tables to writing services is created.

    In: Database schema, Service definitions · Out: Table-to-service write operation map

    ch10 · ch11

  2. Step 2Categorize each table's ownership scenario: single, common, or joint ownership.

    Entry: Write operations are mapped to services.

    Exit: Each table is categorized into an ownership scenario.

    In: Table-to-service write operation map · Out: Categorized table ownership list

    ch10

  3. Step 3Resolve each ownership scenario using an appropriate pattern.

    Entry: Ownership scenarios are categorized.

    Exit: A clear ownership and access pattern is defined for every table.

    • Which technique is best for resolving a joint ownership scenario based on performance, consistency, and coupling trade-offs?

    In: Categorized table ownership list · Out: Defined data ownership structure

    ch10 · ch11

  4. Step 4Implement the chosen patterns, refactoring services and communication as needed.

    Entry: Ownership patterns have been selected.

    Exit: Services are updated to reflect the new data ownership rules.

    In: Defined data ownership structure · Out: Refactored service code

    ch10 · ch11

Analyzing Service Granularity

To determine the optimal size and scope for services in a distributed architecture by balancing forces for separation (disintegrators) and consolidation (integrators).

When to use: When deciding whether to split a service into smaller ones or merge multiple services into one.

  1. Step 1Identify a service or functionality for granularity analysis.

    Entry: There is a question about the appropriate size of a service.

    Exit: A clear target for analysis is defined.

    Out: Service/functionality to be analyzed

    ch08

  2. Step 2Analyze the service using 'granularity disintegrators'—forces that suggest splitting the service.

    Entry: Target for analysis is defined.

    Exit: A list of arguments for splitting the service is documented.

    In: Code change metrics, Performance data, Security requirements · Out: Disintegrator analysis

    ch08

  3. Step 3Analyze the service using 'granularity integrators'—forces that suggest merging services.

    Entry: Target for analysis is defined.

    Exit: A list of arguments for merging services is documented.

    In: Transaction requirements, Workflow diagrams · Out: Integrator analysis

    ch08

  4. Step 4Discuss the analysis with team members and stakeholders to weigh the pros and cons.

    Entry: Both disintegrator and integrator analyses are complete.

    Exit: A consensus on the direction is forming.

    In: Disintegrator analysis, Integrator analysis

    ch08

  5. Step 5Make a final architectural decision on service granularity and document it in an ADR.

    Entry: Team discussion has concluded.

    Exit: A documented decision on service granularity is finalized.

    • Consolidate, separate, or maintain the current service structure?

    In: Team consensus · Out: Architectural Decision Record (ADR)

    ch08

Managing Data Access Across Services

To provide services with access to data owned by other services while balancing performance, consistency, and coupling.

When to use: When a service needs to read or query data that it does not own.

  1. Step 1Identify the data access requirement: which service needs what data from another service.

    Entry: A service has a dependency on data owned by another service.

    Exit: The specific data access need is clearly defined.

    In: Service requirements · Out: Data access requirement specification

    ch12

  2. Step 2Evaluate and select an appropriate data access pattern based on trade-offs.

    Entry: Data access requirement is defined.

    Exit: A suitable data access pattern is chosen.

    • Which pattern best balances the need for data consistency, performance, and low service coupling?

    In: Data access requirement specification · Out: Selected data access pattern

    ch12

  3. Step 3Implement the chosen pattern.

    Entry: A pattern has been selected.

    Exit: The data access solution is implemented and functional.

    In: Selected data access pattern · Out: Implemented data access solution

    ch12

Managing Code Reuse in Distributed Architectures

To select and implement an appropriate strategy for sharing code and functionality across services, balancing reuse with service independence.

When to use: When common, cross-cutting concerns (e.g., logging, authorization, domain logic) are identified across multiple services.

  1. Step 1Identify the shared functionality and analyze its rate of change (volatility).

    Entry: A need for shared functionality across services is identified.

    Exit: The shared functionality and its change frequency are understood.

    In: Service requirements, Historical data on code changes · Out: Analysis of shared functionality

    ch09

  2. Step 2Conduct a trade-off analysis to choose a reuse strategy: shared library, shared service, or replication.

    Entry: Shared functionality is analyzed.

    Exit: A code reuse strategy is selected.

    • Choose between shared library and shared service based on code volatility and dependency management concerns.

    In: Analysis of shared functionality · Out: Selected code reuse strategy

    ch09

  3. Step 3Implement the chosen strategy, including versioning and communication plans.

    Entry: A strategy has been selected.

    Exit: The shared code solution is implemented.

    In: Selected code reuse strategy · Out: Shared library or shared service

    ch09

  4. Step 4For shared operational concerns, consider implementing the Sidecar pattern.

    Entry: There is a need to standardize operational capabilities across services.

    Exit: A sidecar component is designed and integrated.

    • Which operational capabilities should be included in the sidecar?

    In: Operational requirements · Out: Sidecar component

    ch09

Designing and Evaluating Distributed Workflows

To design, evaluate, and implement workflows that coordinate actions across multiple services in a distributed system.

When to use: When designing a multi-service business process, such as order placement or customer unsubscribing.

  1. Step 1Identify and model the business workflow, including all required service interactions.

    Entry: A multi-service workflow needs to be designed.

    Exit: The sequence of steps and service calls is understood.

    In: Business process requirements · Out: Workflow model

    ch14

  2. Step 2Create two candidate architectural solutions: one using orchestration and one using choreography.

    Entry: Workflow model is complete.

    Exit: Two distinct architectural models are created.

    In: Workflow model · Out: Orchestration model, Choreography model

    ch13 · ch14

  3. Step 3Evaluate each solution against key concerns using a trade-off matrix.

    Entry: Candidate solutions are modeled.

    Exit: A trade-off matrix comparing the two approaches is complete.

    In: Orchestration model, Choreography model · Out: Trade-off matrix

    ch14

  4. Step 4Choose the most effective solution and document the decision in an ADR.

    Entry: Trade-off analysis is complete.

    Exit: A final decision on the workflow pattern is made and documented.

    • Which pattern better addresses the most critical concerns for this specific workflow?

    In: Trade-off matrix · Out: Architecture Decision Record (ADR)

    ch14

  5. Step 5Implement the chosen workflow, including a strategy for state management and eventual consistency.

    Entry: A workflow pattern has been selected.

    Exit: The distributed workflow is implemented and functional.

    In: Selected workflow pattern · Out: Implemented workflow

    ch10 · ch11 · ch13

Managing Analytical Data

To design and implement a strategy for consolidating and analyzing operational data for business intelligence and reporting.

When to use: When building or evolving a platform for analytics and business intelligence.

  1. Step 1Evaluate traditional analytical data patterns like the Data Warehouse and Data Lake.

    Entry: A need for an analytical data solution is identified.

    Exit: The trade-offs of traditional patterns are understood.

    In: Analytical requirements · Out: Analysis of traditional data patterns

    ch16

  2. Step 2Consider implementing a Data Mesh framework for a decentralized approach.

    Entry: The limitations of centralized data warehouses or lakes are a concern.

    Exit: The feasibility of a Data Mesh approach is assessed.

    • Does the organization have the culture and technical capability to support a decentralized, domain-driven data approach?

    In: Organizational structure, Domain definitions · Out: Data Mesh feasibility analysis

    ch16

  3. Step 3If using Data Mesh, establish domain ownership and treat data as a product.

    Entry: A decision to implement Data Mesh has been made.

    Exit: Domain data product owners are established and data products are being defined.

    In: Domain teams · Out: Domain-owned data products

    ch16

  4. Step 4Implement a self-service data platform and federated governance.

    Entry: Domain ownership is established.

    Exit: A functional Data Mesh platform and governance model are in place.

    In: Technology platform capabilities, Governance policies · Out: Self-service data platform

    ch16

Documenting Architectural Decisions (ADRs)

To document the rationale behind significant architectural decisions, promoting communication, alignment, and organizational learning over time.

When to use: As an ongoing practice whenever a significant architectural choice is made.

  1. Step 1Create an ADR document with a title indicating the architectural decision.

    Entry: A significant architectural decision has been made.

    Exit: A new ADR file is created.

    In: Architectural decision · Out: ADR document

    ch01 · ch03

  2. Step 2In the 'Context' section, describe the problem and list the alternative solutions considered.

    Entry: ADR document is created.

    Exit: The Context section is complete.

    In: Problem statement, Alternative solutions

    ch01

  3. Step 3In the 'Decision' section, specify the chosen solution and justify the choice.

    Entry: The Context section is complete.

    Exit: The Decision section is complete.

    In: Chosen solution, Justification

    ch01

  4. Step 4In the 'Consequences' section, describe the results of the decision and the trade-offs accepted.

    Entry: The Decision section is complete.

    Exit: The Consequences section is complete and the ADR is finalized.

    In: Trade-off analysis · Out: Completed ADR

    ch01

Implementing Architectural Fitness Functions

To automate the governance of architectural principles and ensure that the implemented system adheres to the intended design over time.

When to use: As part of the continuous integration and deployment process to provide ongoing feedback on architectural compliance.

  1. Step 1Define the specific architectural characteristics that need to be monitored or validated.

    Entry: A key architectural principle needs to be enforced.

    Exit: A clear, measurable rule is defined.

    • Which architectural characteristics are most critical to govern?

    In: Architectural principles · Out: Fitness function criteria

    ch01 · ch06

  2. Step 2Create a fitness function to objectively assess the characteristic.

    Entry: Fitness function criteria are defined.

    Exit: An automated test (the fitness function) is created.

    In: Fitness function criteria, Suitable assessment tools · Out: Fitness function code

    ch01 · ch06

  3. Step 3Integrate the fitness function into the continuous integration (CI) pipeline.

    Entry: The fitness function is created.

    Exit: The fitness function is part of the automated build process.

    In: Fitness function code, CI/CD pipeline configuration

    ch01 · ch06

  4. Step 4Monitor the results and resolve any detected violations.

    Entry: The fitness function is running in the CI pipeline.

    Exit: Architectural violations are actively monitored and fixed.

    • How should the team react to a fitness function failure?

    In: Fitness function results · Out: Resolution of architectural violations

    ch01

The story

The reader Software architects and senior engineers responsible for designing, evolving, or migrating distributed systems—especially those grappling with microservices—who want to make defensible, well-reasoned structural and data decisions under conditions of genuine uncertainty.

External problem

They face architecture decisions in distributed systems for which no generic best practice exists: how to decompose a monolith, how to assign data ownership, how to manage distributed transactions, how to size services, and how to coordinate workflows—each unique to their organization's context.

Internal problem

They feel uncertain, overwhelmed, and exposed—making consequential decisions based on gut feel, anecdote, or oversimplified online advice while knowing the stakes include system reliability, team velocity, and business continuity.

Philosophical problem

It is wrong that architects must navigate some of the highest-stakes technical decisions in a company with the least guidance, armed with platitudes like 'decouple everything' that collapse under real-world constraints.

The plan

  1. Understand what makes distributed architecture decisions hard by separating static and dynamic coupling, and learning the architecture quantum as the unit of analysis.
  2. Build the business case for architectural change by mapping modularity drivers (maintainability, testability, deployability, scalability, fault tolerance) to observed system symptoms.
  3. Decompose the monolith systematically using the six component-based decomposition patterns to arrive at well-formed domain services.
  4. Decompose monolithic data using the five-step data domain process, guided by disintegrators and integrators, and select the appropriate database type for each domain.
  5. Determine correct service granularity by explicitly weighing granularity disintegrators against integrators and documenting the trade-offs with business stakeholders.
  6. Design code reuse appropriately using code replication, shared libraries, shared services, or sidecars depending on volatility, heterogeneity, and coupling requirements.
  7. Assign data ownership precisely (single, common, or joint scenarios) and choose distributed transaction strategies from the eventual consistency pattern catalog.
  8. Solve distributed data access using the four patterns: interservice communication, column schema replication, replicated caching, or data domain sharing.
  9. Design distributed workflows by choosing orchestration versus choreography based on workflow complexity, error frequency, and scale requirements.
  10. Select the appropriate transactional saga pattern from the eight-pattern matrix based on communication, consistency, and coordination requirements.
  11. Manage analytical data separately from operational data using data mesh and data product quanta.
  12. Build a personal trade-off analysis capability: find entangled dimensions, model MECE combinations, iterate with domain scenarios, and communicate bottom-line trade-offs to stakeholders.

Success

  • Architects make and document defensible, context-specific decisions instead of defaulting to received wisdom or vendor recommendations.
  • System migrations from monolith to distributed architecture proceed in a controlled, incremental fashion rather than as unstructured 'big bang' rewrites.
  • Data ownership is clear, distributed transactions are handled with appropriate eventual consistency patterns, and data access across service boundaries uses the least-coupled viable pattern.
  • Service sizes are justified by explicit trade-off analysis rather than opinion, reducing both over-granularity (distributed monolith) and under-granularity (big ball of distributed mud).
  • Architecture governance is automated via fitness functions, preventing architectural drift without requiring constant manual oversight.
  • Business stakeholders can participate meaningfully in architecture trade-off conversations because architects present bottom-line choices rather than technical minutiae.
  • Teams collaborate across application and data disciplines to solve architectural problems jointly rather than in organizational silos.

At stake

  • Without trade-off discipline, systems degrade into big balls of distributed mud—all the operational complexity of microservices with none of the benefits.
  • Distributed transactions managed with naive compensating updates cascade into unrecoverable inconsistency and catastrophic user-facing failures.
  • Shared databases remain a single point of failure and change-control bottleneck even after services are decomposed, negating modularity benefits.
  • Service granularity set by opinion rather than analysis produces either chatty, fragile fine-grained meshes or coarse-grained services that defeat the purpose of decomposition.
  • Code reuse patterns chosen without trade-off analysis produce either brittle shared-library coupling that defeats independent deployment or shared-service fan-out that destroys fault tolerance.
  • The business loses the support-contract line, lays off staff, and the architects are reassigned or let go—the literal stakes of the Sysops Squad saga.

Chapter by chapter

  1. ch01What Happens When There Are No “Best Practices”?

    This chapter discusses the unique challenges faced by software architects in making decisions without established best practices, emphasizing the importance of weighing trade-offs and making informed choices.

  2. ch02Discerning Coupling in Software Architecture

    In navigating the complexities of distributed architectures and microservices, architects must understand and wisely manage coupling to create scalable, maintainable systems.

    • Coupling cannot be dismissed as merely a negative aspect of software architecture; it can positively influence system performance when applied thoughtfully.
    • Successful architects discern the right balance of coupling to ensure that services communicate effectively without creating unnecessary dependencies.
    • The architecture quantum framework empowers architects to evaluate their service designs based on criteria that prioritize independent deployability and functional cohesion.
    • Transforming theoretical concepts into practical applications is essential for today’s architects who face complex, distributed systems that demand nuanced decision-making.
  3. ch03Architectural Modularity

    Addison and Austen grapple with the dire state of their monolithic Sysops Squad application, realizing that architectural modularity might be the key to salvaging their project from collapse.

    • Architectural modularity is not merely a technical solution; it’s a strategic necessity in the face of ever-increasing business demands.
    • The ability to quickly adapt software architecture to match business needs can directly influence a company’s competitive position in the marketplace.
    • Scalability, agility, maintainability, and fault tolerance are critical benefits that modularity offers, making a compelling case for refactoring legacy systems.
    • Effective advocacy for architectural changes must tie the technical merits to concrete business outcomes in order to secure cross-organizational support.
  4. ch04Architectural Decomposition

    Addison and Austen navigate the complexities of decomposing a monolithic application, ultimately deciding between tactical forking and component-based decomposition to redefine their architecture effectively.

    • 'Eating the elephant' is a metaphor for handling monolithic applications but can lead to unstructured, inefficient outcomes if not approached methodically.
    • The Elephant Migration Anti-Pattern serves as a cautionary tale highlighting the pitfalls of loose, incremental decomposition efforts.
    • Component-based decomposition is ideal for codebases with well-defined boundaries, minimizing risks associated with code duplication and maintainability.
    • Tactical forking offers a pragmatic alternative for chaotic codebases, favoring deletion over extraction to facilitate immediate progress.
  5. ch05Component-Based Decomposition Patterns

    This chapter delves into the component-based decomposition approach for migrating monolithic applications to a distributed architecture, detailing specific decomposition patterns essential for this transformation.

    • Component-based decomposition patterns are essential for transitioning from monolithic to distributed architectures, significantly simplifying migration.
    • The 'Identify and Size Components Pattern' facilitates a clear understanding of component complexity, essential for effective decomposable architecture.
    • Shared domain logic can lead to significant efficiency gains when consolidated appropriately, showing the value of the 'Gather Common Domain Components Pattern.'
    • Maintaining a flat architecture by eliminating orphaned classes allows for clearer component definitions and better system maintainability.
  6. ch06Component-Based Decomposition Patterns

    This chapter emphasizes the significance of creating component domains to refactor monolithic applications, establishing a structured path toward a service-based architecture while ensuring appropriate governance through fitness functions.

    • Creating component domains is essential for refactoring monolithic applications into manageable, service-based architectures.
    • Aligning namespaces with domain functionalities clarifies the structure and enhances code maintainability.
    • Employing fitness functions helps govern compliance and prevents unnecessary complexity within domain services.
    • Effective collaboration with business stakeholders is crucial to ensure that technical refactoring meets organizational objectives.
  7. ch07Pulling Apart Operational Data

    Addison and Austen face the challenge of dismantling their monolithic Sysops Squad database into more manageable data domains amidst resistance from a key team member, Dana.

    • The necessity to break apart monolithic databases comes from various operational needs, including scalability, performance, and the management of change.
    • Change control is significantly easier when data is organized within well-defined bounded contexts rather than in a single monolithic structure.
    • Separate databases can enhance scalability and fault tolerance, reducing the risks associated with single points of failure.
    • Engaging stakeholders early and often provides essential support and alignment as teams navigate complex transitions involving data architecture.
  8. ch08Service Granularity

    The quest for optimal service granularity in software architecture lies in balancing the disintegration and integration of services, determining when to break services apart or combine them based on various operational needs.

    • The key to getting service granularity right is to remove opinion and gut feeling and rely on objective analysis.
    • Understanding the distinction between granularity and modularity is crucial for effective architectural decisions.
    • Metrics such as the frequency of code changes play a vital role in informing decisions about service disintegration.
    • Balancing the opposing forces of granularity disintegrators and integrators is fundamental to achieving an optimal architecture.
  9. ch09Reuse Patterns

    In distributed architectures, the debate over the optimal method for code reuse—shared libraries versus shared services—poses significant challenges, as demonstrated in a conflict among software developers regarding the handling of common functionalities.

    • Code reuse is a fundamental but nuanced aspect of software development that requires careful decision-making.
    • The mantra 'reuse is abuse' serves as a caution against the pitfalls of excessive sharing, particularly within distributed systems.
    • Choosing between shared libraries and shared services greatly influences system performance, operational stability, and change management.
    • Fine-grained libraries are generally more manageable than coarse-grained libraries, which can muddle dependency management and increase risk during changes.
  10. ch10Data Ownership and Distributed Transactions

    This chapter explores the intricacies of data ownership within distributed systems and the challenges of managing distributed transactions, emphasizing the critical need for clear ownership structures and transaction management strategies.

    • Clear and explicit data ownership structures are crucial for effective management of distributed architectures.
    • Single ownership is the simplest approach and should be resolved before addressing more complex ownership scenarios.
    • Common ownership should involve a designated primary owner service to avoid pitfalls associated with shared database schemas.
    • Joint ownership requires strategic techniques like table splitting to ensure clarity of data ownership and responsibility.
  11. ch11Data Ownership and Distributed Transactions

    In this chapter, the intricacies of data ownership and management in distributed transactions are explored, showcasing the importance of defining clear boundaries around data access and responsibility among services in modern architecture.

    • Clear data ownership is pivotal in distributed systems; the service that writes to the table must own it.
    • Architecture Decision Records are beneficial tools for documenting and clarifying ownership and operational decisions.
    • Joint-table ownership creates complexities that can be effectively managed through delegation or defined roles.
    • Durability in message processing through durable subscribers is essential for maintaining data integrity during failures.
  12. ch12Distributed Data Access

    This chapter examines the complex challenges of accessing data across distributed systems, outlining specific patterns such as Interservice Communication, Column Schema Replication, Replicated Cache, and Data Domain, each with unique trade-offs and implications.

    • Accessing data across services in a distributed architecture should be approached with a clear understanding of workflow and communication latency.
    • Different data access patterns offer distinct trade-offs that must be navigated based on specific operational needs.
    • The potential for improved performance through caching techniques is countered by the complexities of data synchronization and governance challenges.
    • Collaboration and shared schemas can enhance system integrity at the expense of stricter service boundaries, necessitating careful consideration of risks versus rewards.
  13. ch13Managing Distributed Workflows

    In this chapter, the author explores the coordination patterns—orchestration and choreography—within distributed software architectures, arguing that architects must navigate complex trade-offs to effectively manage workflows.

    • Effective management of distributed workflows necessitates a deep understanding of orchestration and choreography, as both patterns have distinct advantages and challenges.
    • Orchestration serves best in complex workflows requiring centralized state management, while choreography offers flexibility at the cost of increasing error communication complexity.
    • When establishing workflows, prioritizing domain-driven designs ensures that implementations align with business semantics, reducing unnecessary structural complexity.
    • The orchestration model allows for robust error handling strategies, vital for maintaining operational integrity in high-stakes environments.
  14. ch14Contracts

    The chapter examines the critical roles of orchestration and choreography in developing contracts for managing workflows, highlighting the trade-offs architects must navigate to ensure responsiveness, scalability, and fault tolerance.

  15. ch15Transactional Sagas

    The chapter explores the various saga patterns in microservices architecture, detailing their trade-offs and management challenges, particularly with respect to transactional integrity during error conditions.

    • Different saga patterns offer unique trade-offs in managing transactions, particularly under error conditions.
    • The most challenging pattern, the Horror Story, illustrates the pitfalls of overly complex workflows lacking robust transaction management strategies.
    • Anticipating error conditions and designing an effective compensation mechanism are critical for ensuring the resilience of distributed applications.
    • Visual representations can significantly enhance understanding of service interactions and workflows.
  16. ch16Managing Analytical Data

    This chapter explores the complexities of managing analytical data within modern distributed architectures, highlighting the limitations of traditional data warehousing and the innovative promise of data mesh as a solution.

    • The traditional data warehouse model is often ill-suited for modern distributed architectures, leading to inefficiencies and a failure to deliver actionable insights.
    • Data lakes, while more flexible, do not adequately address the need for structured discoverability and governance in analytical processes.
    • The Data Mesh model presents a revolutionary way of managing analytical data, emphasizing domain ownership and product mentality.
    • Establishing Data Product Quanta (DPQ) help organizations maintain the relevance and usability of data in real-time.
  17. ch17Build Your Own Trade-Off Analysis

    In this chapter, architects learn to construct tailored trade-off analyses to navigate the complexities of distributed architectures, emphasizing the interplay of various technical dimensions.

    • Understanding architectural trade-offs is crucial in navigating the complexities of distributed systems.
    • A static coupling diagram is a foundational tool in visualizing the relationships between architectural components.
    • An iterative design approach empowers architects to discover the nuanced impacts of their design choices.
    • Quality analysis, rather than extensive data aggregation, provides clearer insights into architectural trade-offs.

Questions this book answers

When should a monolith be decomposed into distributed services, and how should that decomposition be done?
How do you correctly assign data ownership, manage distributed transactions, and achieve eventual consistency across services?
When should you use orchestration versus choreography for distributed workflows?
How do you choose the right service granularity, balancing disintegrating and integrating forces?
How do you manage code reuse without creating dangerous coupling in distributed systems?

Related in the library

Tools these methods power