English

Software architecture is the set of decisions that determine how a system is organized: which components exist, what each is responsible for, how they communicate, what dependencies are permitted, and how the whole responds to changing requirements over time. These decisions are architectural rather than implementation-level not because they are inherently complex — often the decisions are simple — but because they are difficult to change. A choice to store session state on each server rather than in a shared store becomes expensive to reverse when you need horizontal scaling. A choice to couple services through a shared database becomes expensive to reverse when you need to change the schema for one without affecting the other. Architectural decisions create the constraints within which all subsequent work happens, which is why getting them right — or at least not catastrophically wrong — is worth deliberate attention.

The subject is not about drawing diagrams. Diagrams are a byproduct of architectural thinking; the thinking is about the structure that makes a system maintainable, evolvable, and reliable over the time scales real systems must last. The questions that architecture addresses are the ones that cannot be answered by writing more code: How do we keep parts of the system from knowing too much about other parts? How do we make the boundaries between components explicit and enforceable? How do we structure the system so that changes in requirements require changes to as few components as possible? These questions have answers that are sometimes mathematical, sometimes empirical, always specific to context.

Software architecture draws on the theoretical foundations of this guide — the modular decomposition that stems from information hiding in §3.4, the formal specification traditions of §3.6, the concurrency and consistency models of §4.6 — but it also requires judgment that theory cannot fully supply. Real architectural decisions involve incomplete information, organizational constraints, and human factors that formal models elide. The experienced architect reasons from principles and patterns while accounting for the specific context; the inexperienced architect applies patterns without understanding the principles, or applies principles without understanding the patterns.

Prerequisites: Programming (§2.1) — you need implementation experience to understand what architectural decisions cost. Databases (§4.4) — data architecture is a primary concern. Distributed systems (§4.6) — modern architectures are distributed. Operating systems (§4.2) — deployment context shapes architectural choices.

From Subroutines to Evolutionary Architecture

Software architecture as a named discipline is young. Fifty years ago there was no such thing; there was programming, and programs that became large enough to organize were organized according to the instincts of whoever was building them. The concepts that would become architectural theory developed gradually, often in response to specific failures.

The earliest architectural insight was functional decomposition: break a program into subroutines, one per logical task. This was implicit in the first high-level languages and explicit by the mid-1960s. But functional decomposition alone did not prevent programs from becoming hard to understand and change; the subroutines could be entangled through shared global state, making it impossible to change any one without understanding all the others. The discipline needed a concept of separation, not just division.

Edsger Dijkstra articulated the concept that would become foundational in his 1968 paper “The Structure of ‘THE’-Multiprogramming System,” which described a layered operating system in which each layer used only the services of layers below it and provided services to layers above it. The layered structure enforced a dependency direction: a higher layer could know about lower layers, but not vice versa. This gave the system a topology that made reasoning tractable: to understand layer n, you needed to understand layers 0 through n-1, but not above. Dijkstra’s separation of concerns — the principle that each part of a program should address a distinct concern, and that these concerns should not be mixed — became one of the load-bearing concepts of software engineering.

David Parnas, in his 1972 paper “On the Criteria to Be Used in Decomposing Systems into Modules,” attacked the question of how to divide a system into modules. The prevailing answer was functional: one module per algorithm or function. Parnas argued for a different criterion: information hiding. Each module should hide a design decision that might change, so that changes to that decision require changes to only that module. A module that hides how data is sorted internally from modules that need sorted data means that changing the sorting algorithm requires changing only the module that hides it, not all the modules that use sorted data. Parnas’s criterion was empirical and prescient: the decisions that most often change in software are the ones that should be hidden in modules. Information hiding is now so standard that its status as an insight is easy to miss.

The 1970s and early 1980s brought the proliferation of structured design methods — Constantine and Yourdon’s coupling and cohesion metrics, the data flow diagrams of structured analysis, the entity-relationship model for data. These methods provided vocabulary — high cohesion within modules, low coupling between them — that is still used today, though the specific methods have been largely superseded. The structured methods captured something real about what made software tractable, but they were limited by their focus on batch processing systems and their neglect of object-oriented design.

The object-oriented movement of the 1980s introduced a different architectural organizing principle: encapsulate data and the operations on it together in objects, and allow objects to communicate through message passing. Smalltalk (1972) had been the first fully object-oriented language; C++ (1983) and Eiffel (1985) brought object-orientation to system programming. The promise was that objects would be natural units of decomposition — each object representing a real-world entity — and that inheritance would enable reuse. The reality was more complicated: inheritance created tight coupling between base and derived classes, objects representing real-world entities were often the wrong unit of decomposition, and the promises of reuse through class libraries were not as well-realized as hoped.

The Gang of Four’s Design Patterns: Elements of Reusable Object-Oriented Software (Gamma, Helm, Johnson, and Vlissides, 1994) was a response to the specific ways object-oriented design was going wrong. Rather than advocating a single methodology, it catalogued twenty-three patterns — recurring solutions to recurring design problems — with explanations of when each applied and what tradeoffs it involved. Observer, Strategy, Factory, Decorator, Composite — these patterns gave developers a vocabulary for communicating design intent and a set of proven solutions to common problems. The book has been criticized for promoting pattern-matching over thinking, and for providing solutions that are more complex than necessary in languages with first-class functions; the criticism is partly valid. But the insight that design problems recur across contexts and that proven solutions can be named and documented was genuine.

Grady Booch, Ivar Jacobson, and James Rumbaugh developed the Unified Modeling Language (UML) in the mid-1990s, attempting to standardize the notation for object-oriented design. UML provided class diagrams, sequence diagrams, component diagrams, deployment diagrams, and other diagram types. The ambition was to make software design as rigorous and communicable as engineering diagrams in other disciplines. The result was more mixed: UML diagrams communicate some aspects of design well, others poorly, and the weight of the full notation became counterproductive. Modern practice uses selected UML diagram types — particularly sequence diagrams and class diagrams for specific communication purposes — without attempting full compliance with the UML specification.

Mary Shaw and David Garlan’s work on software architecture in the early 1990s was the first systematic attempt to treat software architecture as a discipline with its own vocabulary and concerns. Their 1996 book Software Architecture: Perspectives on an Emerging Discipline catalogued architectural styles — pipe-and-filter, layered systems, event-driven, client-server, blackboard — and argued that each represented a pattern of component organization with specific properties. This was the architectural analogy to the Gang of Four’s design patterns: recurring structural solutions at a larger scale than individual design patterns. The vocabulary of architectural styles — which is also the vocabulary of most distributed system discussions — traces to this work.

The 2000s brought two developments that changed architectural practice. Martin Fowler’s Patterns of Enterprise Application Architecture (2002) documented the patterns that had emerged in the decade of enterprise object-oriented development: Domain Model, Transaction Script, Active Record, Data Mapper, Repository, Unit of Work, Service Layer, and dozens of others. The book captured what practitioners had learned about organizing the persistent state, business logic, and presentation concerns of enterprise applications. Eric Evans’s Domain-Driven Design (2003) provided a complementary framework: the idea that software should be organized around a carefully developed model of the domain it serves, expressed in a shared language (the Ubiquitous Language) between domain experts and developers, with tactical patterns (Entities, Value Objects, Aggregates, Repositories, Services) and strategic patterns (Bounded Contexts, Context Maps, Anti-Corruption Layers) for structuring large systems.

Microservices — the architectural style in which a system is composed of small, independently deployable services each responsible for a bounded domain — emerged from practice at Amazon, Netflix, and other web-scale companies in the late 2000s. The motivation was organizational as much as technical: large teams working on a monolith created coordination overhead that slowed delivery; decomposing the monolith into services allowed teams to work independently. The microservices movement provided architectural patterns for this decomposition, but it also introduced the challenges of distributed systems that §4.6 describes: network failures, eventual consistency, distributed tracing, operational complexity. Sam Newman’s Building Microservices (2015) organized the emerging practice.

The contemporary period has seen a maturation of the architectural conversation. The “microservices vs. monolith” debate, once theological, has become more empirical: the consensus is that monoliths are appropriate for systems where the team coordination overhead of microservices is not justified, and that microservices impose real costs that must be offset by real benefits. Neal Ford, Rebecca Parsons, and Patrick Kua’s Building Evolutionary Architectures (2017) provided a framework for architectural fitness functions — automated tests that verify architectural properties — and for designing systems that can evolve without large-scale refactoring. The idea that architecture is not a one-time design act but an ongoing engineering discipline is central to contemporary architectural thinking.

Coupling, Modularity, and Architectural Styles

The Organizing Principles

Architecture is ultimately about managing dependencies. Every decision about which module knows about which other module, which service calls which service, which database table is read by which component — is a dependency decision. Dependencies create coupling: when A depends on B, changes to B may require changes to A. High coupling makes systems hard to change because changes propagate; low coupling makes systems easy to change because changes stay local.

Coupling has multiple dimensions. Afferent coupling measures how many components depend on a given component; a component with high afferent coupling is difficult to change because many other components must be updated when it changes. Efferent coupling measures how many other components a given component depends on; a component with high efferent coupling is fragile because it can be broken by changes to any of its dependencies. The stability of a component — how likely it is to change — determines the cost of depending on it. Robert Martin’s Stable Dependencies Principle states that components should depend in the direction of stability: volatile components should not be depended upon by stable ones.

Cohesion measures how related the responsibilities of a component are. A class that handles user authentication, generates PDF reports, and sends email notifications has low cohesion; any of those responsibilities might need to change for reasons unrelated to the others. A class that handles only user authentication has high cohesion; changes to authentication logic affect only this class. High cohesion and low coupling are not independent: they are two aspects of the same underlying principle that components should have a single, well-defined reason to change.

The Dependency Inversion Principle — that high-level modules should not depend on low-level modules; both should depend on abstractions — provides a mechanism for achieving low coupling at architectural scale. By defining interfaces that represent the capabilities that high-level modules need, and having low-level modules implement those interfaces rather than being directly depended upon, the direction of source code dependencies can be inverted relative to the direction of control flow. This inversion allows the core business logic of a system to be independent of its infrastructure — databases, user interfaces, external services — making it possible to test business logic without infrastructure and to change infrastructure without affecting business logic.

Architectural Styles and Their Tradeoffs

Layered architecture organizes components into layers, with each layer depending only on the layer below it. A classic three-tier web application has a presentation layer, a business logic layer, and a data access layer. Layering simplifies reasoning about dependencies — to understand a layer, you only need to understand the layers it depends on — and enables substitution of layers. The cost is that adding or changing cross-cutting concerns requires changes in multiple layers, and that the strict layer boundary can force artificial routing of interactions through intermediate layers when a direct interaction would be simpler.

Event-driven architecture decouples producers from consumers by having producers emit events to a bus or queue, with consumers subscribing to the events they care about. The producer does not know who is listening; the consumer does not know who produced the event. This eliminates direct dependencies between producers and consumers, making it easy to add new consumers without changing producers. The cost is that the flow of logic is difficult to trace — an event triggers a chain of handlers that may themselves emit further events — and that debugging becomes harder because execution is non-linear. The same mechanism that makes the architecture flexible makes it hard to understand.

Microservices architecture decomposes a system into small, independently deployable services, each responsible for a bounded domain and each owning its own data. Each service is a separate deployment unit with its own code repository, build pipeline, and database. The architectural benefits are independent deployability (one service can be deployed without coordinating with others), team autonomy (each service is owned by a team that can make its own decisions), and resilience (failure in one service does not directly cause failure in others). The costs are the full costs of distributed systems: network latency and failure, eventual consistency, distributed tracing, complex operational management, and the difficulty of transactions that span service boundaries.

The monolith — a system that is deployed as a single unit — is often presented as the opposite of microservices and inferior to it. The reality is more nuanced. A well-structured monolith with clear module boundaries provides most of the benefits of microservices — independent development of modules, clear interfaces between concerns — without the operational overhead of managing many services. The Majestic Monolith (coined by DHH) and the Modular Monolith (as described by Sam Newman in Monolith to Microservices) describe monolithic systems with internal module boundaries designed to make future decomposition possible if needed. The monolith-first approach — build a monolith, understand the domain, decompose into services when the cost of the monolith becomes demonstrable — avoids the trap of microservices decomposition before the domain is understood well enough to draw the right boundaries.

Architectural Decision Records

Architectural decisions are made in context that changes over time. The reasons a team made a specific architectural choice — what alternatives were considered, what constraints existed, what tradeoffs were acceptable — are lost within months if they are not recorded. Six months later, when the team revisits the decision, they must either accept a decision they do not understand or reconstruct the reasoning from scratch.

Architectural Decision Records (ADRs) — short documents recording a specific architectural decision, its context, the alternatives considered, and the consequences — address this problem. The MADR (Markdown Architectural Decision Records) format and Michael Nygard’s original proposal specify a simple structure: title, status (proposed / accepted / deprecated / superseded), context, decision, and consequences. ADRs live in the code repository alongside the code they describe, so they version with the code. When a decision is revisited, the ADR explains the original reasoning; if the decision changes, the old ADR is marked superseded and a new one is added.

The act of writing an ADR is as valuable as the ADR itself. Articulating the context, alternatives, and tradeoffs forces the precision that informal discussion often avoids. Decisions that seem obvious become less obvious when they must be stated clearly enough that a new team member would understand the reasoning.

What Studying This Changes

Software architecture changes how practitioners think about structure before and during implementation.

The first change is the habit of making coupling explicit. Before adding a dependency — one module importing another, one service calling another, one component sharing data with another — the architectural-thinking practitioner asks: what does this dependency cost? Is this coupling necessary, or can it be inverted? Does this dependency run in the direction of stability? This discipline does not eliminate all coupling — coupling is unavoidable — but it makes coupling decisions deliberate rather than accidental.

The second change is the ability to evaluate architectural claims empirically. The discourse around software architecture — monolith versus microservices, REST versus GraphQL, SQL versus NoSQL — is often more theological than empirical. Practitioners who have studied architectural tradeoffs can ask: what specific problems does this choice solve, and at what cost? What are the operational requirements? What is the team’s experience with distributed systems? These questions produce context-specific answers rather than context-independent proclamations.

The third change is vocabulary for communicating design intent. Bounded Context, Anti-Corruption Layer, Repository, Event Sourcing, CQRS — these terms communicate design intent precisely to practitioners who know them. A conversation that without these terms requires ten minutes of explanation takes one minute with them. The vocabulary is not an end in itself; it is the medium through which design decisions can be discussed and critiqued efficiently.

The fourth change is the ability to evolve architecture over time. Systems change; requirements change; teams change. Architecture designed as if the current state of knowledge is permanent creates technical debt that accumulates until the system must be rewritten. Practitioners who understand evolutionary architecture — fitness functions, strangler fig patterns, incremental decomposition — can treat architecture as an ongoing concern rather than a one-time decision.

Resources

Books and Texts

Ford, Richards, Sadalage, and Dehghani’s Software Architecture: The Hard Parts (O’Reilly, 2021) is the most valuable contemporary text on distributed system architecture. It addresses the genuinely difficult tradeoffs — when to break apart data, how to handle distributed transactions, when to synchronize or choreograph — with the specificity that survey texts omit. It assumes comfort with microservices and distributed systems concepts and provides the reasoning frameworks for the hardest decisions in that space.

Richards and Ford’s Fundamentals of Software Architecture (O’Reilly, 2020) is a more accessible entry, covering the foundational concepts — coupling and cohesion, architecture styles, architecture characteristics, decision-making — with breadth rather than depth. It is the right starting point for practitioners who want a comprehensive overview before specializing.

Evans’s Domain-Driven Design: Tackling Complexity in the Heart of Software (Addison-Wesley, 2003) is the foundational text for domain-centric architecture. The tactical patterns (Entities, Value Objects, Aggregates, Repositories) and strategic patterns (Bounded Contexts, Ubiquitous Language, Anti-Corruption Layers) are the primary vocabulary of contemporary microservices architecture. Dense and demanding; the more accessible Implementing Domain-Driven Design by Vaughn Vernon provides a practitioner-oriented complement.

Fowler’s Patterns of Enterprise Application Architecture (Addison-Wesley, 2002) remains authoritative for the data architecture patterns that most enterprise and web applications use. The catalog of patterns — Domain Model, Data Mapper, Repository, Unit of Work, Service Layer — is stable across technology changes and provides the vocabulary for discussing how business logic and persistence interact.

Robert Martin’s Clean Architecture: A Craftsman’s Guide to Software Structure and Design (Prentice Hall, 2017) provides the dependency management principles (Stable Abstractions, Stable Dependencies, Acyclic Dependencies) and the architectural vision of concentric rings separating business logic from infrastructure. The principles are sound even where the specific prescriptions are sometimes overstate.

Newman’s Building Microservices (2nd ed., O’Reilly, 2021) is the standard reference for microservices architecture — communication patterns, data management, service decomposition, and operations. The second edition significantly updated the first in response to experience with patterns that proved problematic.

Ford, Parsons, and Kua’s Building Evolutionary Architectures (2nd ed., O’Reilly, 2022) introduces architectural fitness functions — automated tests of architectural properties — and provides a framework for designing architectures that can evolve without accumulating debt. Particularly valuable for teams that need to treat architecture as ongoing engineering rather than one-time design.

Book Role Type
Ford, Richards et al., Software Architecture: The Hard Parts Distributed architecture decisions depth Depth
Richards & Ford, Fundamentals of Software Architecture Accessible broad entry Entry
Evans, Domain-Driven Design DDD foundational text Depth
Vernon, Implementing Domain-Driven Design DDD practitioner-oriented complement Depth
Fowler, Patterns of Enterprise Application Architecture Data and business logic patterns Depth
Martin, Clean Architecture Dependency management principles Depth
Newman, Building Microservices (2nd ed.) Microservices standard reference Reference
Ford, Parsons & Kua, Building Evolutionary Architectures (2nd ed.) Evolutionary architecture practices Practice
Garlan & Shaw, Software Architecture Historical foundational text Depth
Gamma et al., Design Patterns GoF design patterns Reference
Brown & Wilson, The Architecture of Open Source Applications (free; paid print optional) Real-world open-source architecture case studies Practice

Courses, Papers, and Current Sources

Martin Fowler’s blog and website (martinfowler.com, free) is the most consistently valuable single resource on software architecture. The articles on microservices, event sourcing, CQRS, strangler fig, and architectural decision records are authoritative and regularly updated. The Bliki (blog + wiki) format means articles are revised as understanding develops; checking the date of last revision is informative.

Michael Nygard’s original ADR proposal (cognitect.com, free) is the primary source for Architectural Decision Records. The MADR format (adr.github.io, free) provides a structured template.

The ThoughtWorks Technology Radar (thoughtworks.com/radar, free, published twice yearly) tracks emerging practices in software development and architecture with brief assessments of Adopt / Trial / Assess / Hold. Reading several years of radar issues together reveals which practices have sustained interest and which were short-lived fashions.

Resource Platform Type
Fowler, martinfowler.com (free) martinfowler.com Reference
ThoughtWorks Technology Radar (free) thoughtworks.com/radar Reference
Nygard, original ADR proposal (free) cognitect.com Reference
MADR format (free) adr.github.io Reference
AWS Well-Architected Framework (free) docs.aws.amazon.com Reference
Azure Architecture Center / Cloud Design Patterns (free) learn.microsoft.com Reference

Practice, Tools, and Current Sources

The C4 model (c4model.com, free, Simon Brown) provides a hierarchical notation for software architecture diagrams: Context (how the system fits into its environment), Containers (the major deployable components), Components (the major modules within a container), and Code (the implementation detail). The hierarchy allows diagrams at appropriate levels of abstraction for different audiences. The accompanying Structurizr tooling generates C4 diagrams from a DSL, keeping diagrams in sync with the codebase.

ArchUnit (Java, free, archunit.org) and Dependency Cruiser (JavaScript, free, GitHub) are tools for enforcing architectural constraints as automated tests: “this layer should not depend on that layer,” “these packages should not have circular dependencies,” “this module should only depend on modules in this list.” Encoding architectural rules as executable tests makes violations visible in CI rather than in code review.

The arc42 template (free, arc42.org) provides a structure for architectural documentation — context, constraints, solution strategy, building block view, runtime view, deployment view, quality scenarios — that ensures important architectural aspects are considered and recorded.

Resource Platform Type
C4 model + Structurizr (free) c4model.com Practice
ArchUnit (Java, free) archunit.org Practice
Dependency Cruiser (JS, free) GitHub Practice
arc42 template (free) arc42.org Reference

Traps

Trap Why it misleads Better response
Cargo-culting architectural styles Microservices, hexagonal architecture, event sourcing — each is a solution to specific problems, not universally applicable. Applying microservices to a system that a small team builds and deploys adds distributed systems complexity without the team coordination benefits that justify it. Applying event sourcing to a CRUD application adds operational complexity without the auditability or temporal query benefits that justify it. Before adopting an architectural style, articulate the specific problem it solves and verify that the problem exists in the current system. Evans’s DDD strategic patterns are appropriate when domain complexity is genuinely high; for simple CRUD applications, they add ceremony without benefit. The decision to adopt a pattern should be justified by the specific problems it addresses.
Treating architectural diagrams as the architecture Architecture is the set of decisions and their rationale; diagrams are one way to communicate some of those decisions. A beautifully complete UML diagram of a system that was built differently communicates nothing useful; a simple sketch of how data flows through the system, current as of last week, is invaluable. Keep architectural documentation minimal, accurate, and current. ADRs record the decisions; C4 diagrams at the appropriate level of abstraction communicate the structure. Resist the impulse to create comprehensive documentation that will become stale; invest in the documentation that changes the fewest decisions communicate the most.
Designing for anticipated requirements The temptation to design for requirements the system does not currently have but might have in the future produces architecture that is more complex than current requirements justify and that is optimized for futures that may not materialize. This is speculative generalization. Design for current requirements with explicit consideration of which boundaries to make evolvable. The strangler fig pattern, ADRs that record which decisions were made to preserve optionality, and architectural fitness functions that verify the architecture’s flexibility are the tools for managing evolution without over-engineering for anticipated futures.
Ignoring organizational structure Conway’s Law — “organizations which design systems are constrained to produce designs which are the copies of the communication structure of these organizations” — is empirically validated. A system designed by a team organized as frontend/backend/database will have those three tiers as its primary structure, regardless of what the architect specifies. Treat organizational structure as an architectural input. The Inverse Conway Maneuver — designing the team structure to produce the desired system architecture — is a tool for architectural change. Domain-Driven Design’s Bounded Contexts align with team ownership; designing the team boundaries to match the desired service boundaries produces architectures that are maintained by teams with aligned incentives.
Skipping dependency analysis Dependency is the mechanism through which changes propagate; architecture is the discipline of managing dependencies. Practitioners who implement architectures without explicitly analyzing the dependency graph — which components depend on which, in which direction, at what frequency do they change — cannot predict where change will be expensive. Map the dependency graph of any system you are responsible for, using tools (ArchUnit, Dependency Cruiser) where possible. Identify the most-depended-upon components and verify they are the most stable. Identify circular dependencies and resolve them. The dependency map is more informative about architectural health than any diagram that shows what the architecture is supposed to be.

中文

软件架构是一组决定系统如何被组织起来的决策:有哪些组件,每个组件负责什么,它们如何通信,允许哪些依赖,以及整个系统如何随时间回应不断变化的需求。这些决策之所以是架构层面的,而不是实现层面的,并不是因为它们天然复杂——很多时候这些决策很简单——而是因为它们很难改变。选择把 session state 存在每台 server 上,而不是存入 shared store,当你需要 horizontal scaling 时,就会变得很难逆转。选择通过 shared database 耦合 services,当你需要修改其中一个 service 的 schema 而不影响另一个 service 时,也会变得很难逆转。架构决策会创造后续所有工作的约束条件,因此有意识地把它们做对——或者至少不要灾难性地做错——值得投入注意力。

这门学科不是关于画图的。图只是架构思维的副产品;真正的思维对象,是一种结构:它使系统在真实系统必须持续存在的时间尺度上保持可维护、可演化、可靠。架构处理的问题,不是多写代码就能回答的问题:如何防止系统的一部分知道其他部分太多?如何让组件边界明确且可强制执行?如何组织系统,使需求变化时,需要修改的组件尽可能少?这些问题的答案有时是数学的,有时是经验的,但总是依赖具体语境。

软件架构会借助本指南中的理论基础——§3.4 中源自 information hiding 的模块化分解,§3.6 中的形式规约传统,§4.6 中的并发与一致性模型——但它也要求一种理论无法完全提供的判断力。真实架构决策涉及信息不完整、组织约束和人类因素,而这些内容通常会被形式模型省略。经验丰富的架构师会从原则和模式出发,同时纳入具体语境;缺乏经验的架构师则会在不理解原则的情况下套用模式,或在不理解模式的情况下空谈原则。

前置知识:编程(§2.1)——你需要实现经验,才能理解架构决策的成本。数据库(§4.4)——数据架构是主要关切。分布式系统(§4.6)——现代架构是分布式的。操作系统(§4.2)——部署语境会塑造架构选择。

从 Subroutines 到 Evolutionary Architecture

软件架构作为一个命名学科还很年轻。五十年前并没有这样的东西;当时只有编程,而那些大到需要组织的程序,会按照构建者的直觉被组织起来。后来成为架构理论的概念,是逐渐发展出来的,而且往往是对具体失败的回应。

最早的架构洞见是 functional decomposition:把一个程序拆成 subroutines,每个 subroutine 对应一项逻辑任务。这在最早的高级语言中已经隐含存在,到 1960 年代中期变得明确。但 functional decomposition 本身并不能防止程序变得难以理解和修改;subroutines 仍然可能通过 shared global state 纠缠在一起,使你无法在不理解全部其他部分的情况下修改其中任何一个。这个领域需要的是 separation 的概念,而不只是 division。

Edsger Dijkstra 在 1968 年论文 “The Structure of ‘THE’-Multiprogramming System” 中阐述了后来成为基础的概念。该文描述了一个 layered operating system,其中每一层只使用其下方 layers 提供的服务,并向上方 layers 提供服务。Layered structure 强制了依赖方向:上层可以知道下层,但反过来不行。这给系统提供了一种 topology,使推理变得可处理:要理解第 n 层,只需要理解第 0 到 n-1 层,而不需要理解它上面的层。Dijkstra 的 separation of concerns——程序的每一部分都应处理一个 distinct concern,而且这些 concerns 不应混在一起——后来成为软件工程中承重概念之一。

David Parnas 在 1972 年论文 “On the Criteria to Be Used in Decomposing Systems into Modules” 中攻击了“如何把系统划分为 modules”这个问题。当时主流答案是 functional:每个 algorithm 或 function 一个 module。Parnas 主张使用不同标准:information hiding。每个 module 应当隐藏一个可能变化的 design decision,使该决策变化时,只需要修改这个 module。一个向需要 sorted data 的 modules 隐藏内部排序方式的 module,意味着更改 sorting algorithm 时,只需要修改隐藏它的 module,而不必修改所有使用 sorted data 的 modules。Parnas 的标准既经验化,又具有预见性:软件中最常变化的决策,正是最应该被隐藏在 modules 中的决策。Information hiding 今天已经如此标准,以至于人们很容易忘记它曾经是一项洞见。

1970 年代和 1980 年代早期带来了 structured design methods 的扩散——Constantine 和 Yourdon 的 coupling 与 cohesion metrics,structured analysis 中的 data flow diagrams,以及用于数据的 entity-relationship model。这些方法提供了今天仍在使用的词汇——modules 内部 high cohesion,modules 之间 low coupling——尽管具体方法大多已经被取代。Structured methods 捕捉到了某些真实内容:是什么让软件变得可处理。但它们也受限于对 batch processing systems 的关注,以及对 object-oriented design 的忽视。

1980 年代的 object-oriented movement 引入了另一种架构组织原则:把数据和作用于数据的操作一起封装在 objects 中,并允许 objects 通过 message passing 通信。Smalltalk(1972)是第一门完整 object-oriented language;C++(1983)和 Eiffel(1985)则把 object-orientation 带入 system programming。其承诺是 objects 会成为自然的分解单位——每个 object 表示一个现实世界实体——而 inheritance 会支持复用。现实更复杂:inheritance 在 base classes 与 derived classes 之间制造紧耦合;表示现实实体的 objects 往往并不是正确的分解单位;通过 class libraries 实现复用的承诺也没有像预期那样充分实现。

Gang of Four 的 Design Patterns: Elements of Reusable Object-Oriented Software(Gamma、Helm、Johnson 和 Vlissides,1994)是对 object-oriented design 具体失败方式的回应。它不是倡导单一方法论,而是编目了二十三个 patterns——对反复出现的设计问题的反复解决方案——并说明每个 pattern 何时适用、涉及什么权衡。Observer、Strategy、Factory、Decorator、Composite——这些 patterns 为开发者提供了沟通设计意图的词汇,也提供了一组针对常见问题的已验证解决方案。这本书被批评为促进 pattern-matching 而不是思考,也被批评为在拥有 first-class functions 的语言中提供了不必要复杂的解决方案;这些批评部分有效。但它的洞见是真实的:设计问题会跨语境反复出现,而经过验证的解决方案可以被命名和记录。

Grady Booch、Ivar Jacobson 和 James Rumbaugh 在 1990 年代中期发展出 Unified Modeling Language(UML),试图标准化 object-oriented design 的表示法。UML 提供 class diagrams、sequence diagrams、component diagrams、deployment diagrams 以及其他图类型。其抱负是让软件设计像其他工程学科中的工程图一样严格、可沟通。结果更复杂:UML diagrams 能很好沟通某些设计方面,但对另一些方面效果较差;完整表示法的重量也变得适得其反。现代实践会选择性使用 UML diagram types——尤其是 sequence diagrams 和 class diagrams,用于特定沟通目的——而不会试图完全遵循 UML specification。

Mary Shaw 和 David Garlan 在 1990 年代早期关于 software architecture 的工作,是第一次把软件架构作为一门拥有自身词汇和关切的学科进行系统化处理的尝试。他们 1996 年的书 Software Architecture: Perspectives on an Emerging Discipline 编目了 architectural styles——pipe-and-filter、layered systems、event-driven、client-server、blackboard——并主张每一种都代表一种具有特定性质的 component organization pattern。这相当于 Gang of Four design patterns 在架构层面的类比:比单个设计模式更大尺度上的重复结构解决方案。Architectural styles 的词汇——它也是大多数分布式系统讨论中的词汇——可以追溯到这项工作。

2000 年代带来了两个改变架构实践的发展。Martin Fowler 的 Patterns of Enterprise Application Architecture(2002)记录了企业 object-oriented development 十年中形成的 patterns:Domain Model、Transaction Script、Active Record、Data Mapper、Repository、Unit of Work、Service Layer,以及许多其他模式。这本书总结了实践者关于如何组织企业应用中的 persistent state、business logic 和 presentation concerns 的经验。Eric Evans 的 Domain-Driven Design(2003)提供了互补框架:软件应围绕其服务领域中被仔细发展出的 domain model 来组织,并在 domain experts 与 developers 之间用共享语言(Ubiquitous Language)表达;同时使用 tactical patterns(Entities、Value Objects、Aggregates、Repositories、Services)和 strategic patterns(Bounded Contexts、Context Maps、Anti-Corruption Layers)来组织大型系统。

Microservices 是一种 architectural style,其中系统由小型、可独立部署的 services 组成,每个 service 负责一个 bounded domain。它来自 2000 年代后期 Amazon、Netflix 及其他 web-scale companies 的实践。其动机既是技术的,也是组织的:大型团队在 monolith 上工作会产生 coordination overhead,减慢交付;把 monolith 分解为 services 可以让团队独立工作。Microservices movement 为这种分解提供了架构模式,但也引入了 §4.6 所描述的分布式系统挑战:network failures、eventual consistency、distributed tracing、operational complexity。Sam Newman 的 Building Microservices(2015)组织了这种新兴实践。

当代阶段见证了架构讨论的成熟。“microservices vs. monolith” 争论曾经像神学争论,如今变得更经验化:共识是,当 microservices 的 team coordination overhead 收益不足以证明其成本时,monoliths 是合适的;microservices 会带来真实成本,而这些成本必须被真实收益抵消。Neal Ford、Rebecca Parsons 和 Patrick Kua 的 Building Evolutionary Architectures(2017)提供了 architectural fitness functions 的框架——也就是验证架构性质的自动化测试——并提供了设计可演化系统的框架,使系统可以不经大规模重构而继续演化。当代架构思维的核心之一,正是把 architecture 理解为持续工程纪律,而不是一次性设计行为。

Coupling、Modularity 与 Architectural Styles

组织原则

架构最终是关于管理依赖。每一个关于“哪个 module 知道哪个 module”“哪个 service 调用哪个 service”“哪个 database table 被哪个 component 读取”的决策,都是依赖决策。Dependencies 创造 coupling:当 A 依赖 B 时,B 的变化可能要求 A 也变化。High coupling 使系统难以修改,因为变化会传播;low coupling 使系统容易修改,因为变化会保持局部化。

Coupling 有多个维度。Afferent coupling 衡量有多少 components 依赖某个 component;一个 afferent coupling 很高的 component 很难修改,因为修改它时,许多其他 components 也必须更新。Efferent coupling 衡量某个 component 依赖多少其他 components;一个 efferent coupling 很高的 component 很脆弱,因为它可能被任何依赖项的变化破坏。一个 component 的 stability——也就是它多可能变化——决定了依赖它的成本。Robert Martin 的 Stable Dependencies Principle 认为,components 应当朝 stability 的方向依赖:volatile components 不应被 stable components 依赖。

Cohesion 衡量一个 component 的职责之间相关程度。一个同时处理 user authentication、生成 PDF reports、发送 email notifications 的 class 具有 low cohesion;这些职责中的任何一个都可能因与其他职责无关的原因变化。一个只处理 user authentication 的 class 具有 high cohesion;authentication logic 的变化只影响这个 class。High cohesion 和 low coupling 并不是相互独立的:它们是同一个底层原则的两个方面,即 components 应当有一个单一且定义清楚的变化理由。

Dependency Inversion Principle——high-level modules 不应依赖 low-level modules;二者都应依赖 abstractions——提供了一种在架构尺度上实现 low coupling 的机制。通过定义表示 high-level modules 所需能力的 interfaces,并让 low-level modules 实现这些 interfaces,而不是被直接依赖,就可以使 source code dependencies 的方向相对于 control flow 的方向发生反转。这种 inversion 允许系统的 core business logic 独立于 infrastructure——databases、user interfaces、external services——从而可以在没有 infrastructure 的情况下测试 business logic,也可以在不影响 business logic 的情况下更换 infrastructure。

Architectural Styles 及其权衡

Layered architecture 把 components 组织成 layers,每一层只依赖其下方 layer。经典 three-tier web application 包含 presentation layer、business logic layer 和 data access layer。Layering 简化了对 dependencies 的推理——要理解某一层,只需要理解它依赖的层——并允许替换 layers。代价是,添加或改变 cross-cutting concerns 需要修改多个 layers;而严格 layer boundary 也可能迫使交互通过中间层进行人工绕行,即使直接交互本来更简单。

Event-driven architecture 通过让 producers 向 bus 或 queue 发出 events,并让 consumers 订阅自己关心的 events,使 producers 与 consumers 解耦。Producer 不知道谁在监听;consumer 不知道 event 由谁产生。这消除了 producers 与 consumers 之间的直接依赖,使新增 consumers 不需要修改 producers。代价是逻辑流很难追踪——一个 event 会触发一串 handlers,而这些 handlers 又可能发出更多 events——调试也因此更困难,因为执行是非线性的。使架构灵活的同一机制,也使它难以理解。

Microservices architecture 把系统分解为小型、可独立部署的 services,每个 service 负责一个 bounded domain,并拥有自己的数据。每个 service 都是独立部署单位,拥有自己的 code repository、build pipeline 和 database。架构收益包括 independent deployability(一个 service 可以在不与其他 services 协调的情况下部署)、team autonomy(每个 service 由一个团队拥有,并能自行做决策)和 resilience(一个 service 的失败不会直接导致其他 services 失败)。成本则是分布式系统的完整成本:network latency and failure、eventual consistency、distributed tracing、复杂运维管理,以及跨 service boundaries 的 transactions 难题。

Monolith——作为一个单一单位部署的系统——常被呈现为 microservices 的反面,并被认为低于 microservices。现实更细致。一个结构良好、module boundaries 清晰的 monolith,可以提供 microservices 的多数收益——modules 的独立开发、concerns 之间的清晰 interfaces——同时避免管理大量 services 的运维开销。Majestic Monolith(DHH 提出的说法)和 Modular Monolith(Sam Newman 在 Monolith to Microservices 中描述的形式)都指向具有内部 module boundaries 的 monolithic systems,这些边界被设计为在需要时支持未来分解。Monolith-first approach——先构建 monolith,理解 domain,当 monolith 的成本变得可证明时再分解为 services——可以避免在 domain 尚未充分理解、无法画出正确边界之前就过早分解 microservices 的陷阱。

Architectural Decision Records

架构决策是在会随时间变化的语境中做出的。团队做出某个具体架构选择的理由——考虑过哪些 alternatives,存在什么 constraints,哪些 tradeoffs 是可接受的——如果没有记录,几个月内就会丢失。六个月后,当团队重新审视该决策时,他们要么接受一个自己不理解的决定,要么从零重建当初的推理。

Architectural Decision Records(ADRs)——用于记录某个具体架构决策、其 context、考虑过的 alternatives 和 consequences 的短文档——就是为了解决这个问题。MADR(Markdown Architectural Decision Records)format 和 Michael Nygard 的原始提案规定了一个简单结构:title、status(proposed / accepted / deprecated / superseded)、context、decision 和 consequences。ADRs 与它们描述的 code 一起放在 code repository 中,因此会随 code 一起版本化。当某个 decision 被重新审视时,ADR 会解释原始推理;如果 decision 改变,旧 ADR 会被标记为 superseded,并新增一份 ADR。

写 ADR 这个行为本身,与 ADR 文档一样有价值。阐明 context、alternatives 和 tradeoffs,会迫使人达到非正式讨论经常回避的精确度。那些看起来显而易见的 decisions,一旦必须被清楚表述到足以让新团队成员理解其推理,就会变得不那么显然。

学习这一部分会改变什么

软件架构会改变实践者在实现之前和实现过程中思考结构的方式。

第一个变化,是养成把 coupling 显性化的习惯。在添加一个 dependency 之前——一个 module import 另一个 module,一个 service 调用另一个 service,一个 component 与另一个 component 共享数据——具有架构思维的实践者会问:这个 dependency 的成本是什么?这个 coupling 是必要的吗,还是可以被 inverted?这个 dependency 是否朝 stability 的方向运行?这种纪律不会消除所有 coupling——coupling 不可避免——但它会让 coupling decisions 变得有意识,而不是偶然形成。

第二个变化,是能够经验化评估架构主张。软件架构话语——monolith versus microservices、REST versus GraphQL、SQL versus NoSQL——经常更像神学而不是经验分析。学习过架构权衡的实践者会问:这个选择解决了什么具体问题,代价是什么?它有什么 operational requirements?团队是否具备 distributed systems 经验?这些问题会产生依赖语境的答案,而不是脱离语境的宣言。

第三个变化,是拥有沟通设计意图的词汇。Bounded Context、Anti-Corruption Layer、Repository、Event Sourcing、CQRS——这些术语能向懂它们的实践者精确传达设计意图。没有这些术语时需要十分钟解释的对话,有了它们可能一分钟就够。词汇本身不是目的;它是设计决策能被高效讨论和批评的媒介。

第四个变化,是能够随时间演化架构。系统会变化;需求会变化;团队会变化。如果架构设计假定当前知识状态是永久的,就会制造技术债,而这些债会积累到系统必须重写。理解 evolutionary architecture 的实践者——fitness functions、strangler fig patterns、incremental decomposition——可以把 architecture 当成持续关切,而不是一次性决策。

资源

书籍与文本

Ford、Richards、Sadalage 和 Dehghani 的 Software Architecture: The Hard Parts(O’Reilly,2021)是当代分布式系统架构中最有价值的文本。它处理真正困难的权衡——什么时候拆分数据,如何处理 distributed transactions,什么时候使用 synchronization 或 choreography——并提供 survey texts 通常缺少的具体性。它假定读者熟悉 microservices 和 distributed systems 概念,并为这一空间中最困难的 decisions 提供推理框架。

Richards 和 Ford 的 Fundamentals of Software Architecture(O’Reilly,2020)是更易进入的入口,广泛覆盖基础概念——coupling and cohesion、architecture styles、architecture characteristics、decision-making——其优势在广度而非深度。对于想在专门化之前获得综合概览的实践者,这是正确起点。

Evans 的 Domain-Driven Design: Tackling Complexity in the Heart of Software(Addison-Wesley,2003)是 domain-centric architecture 的奠基文本。Tactical patterns(Entities、Value Objects、Aggregates、Repositories)和 strategic patterns(Bounded Contexts、Ubiquitous Language、Anti-Corruption Layers)是当代 microservices architecture 的主要词汇。它密度高、要求高;Vaughn Vernon 的更易读的 Implementing Domain-Driven Design 则提供了面向实践者的补充。

Fowler 的 Patterns of Enterprise Application Architecture(Addison-Wesley,2002)仍然是多数 enterprise 和 web applications 所用 data architecture patterns 的权威参考。其 pattern catalog——Domain Model、Data Mapper、Repository、Unit of Work、Service Layer——跨技术变化保持稳定,并提供了讨论 business logic 与 persistence 如何交互的词汇。

Robert Martin 的 Clean Architecture: A Craftsman’s Guide to Software Structure and Design(Prentice Hall,2017)提供 dependency management principles(Stable Abstractions、Stable Dependencies、Acyclic Dependencies),以及将 business logic 与 infrastructure 分离为同心圆的架构愿景。即使其中一些具体 prescription 有时被夸大,其原则仍然可靠。

Newman 的 Building Microservices(第 2 版,O’Reilly,2021)是 microservices architecture 的标准参考——覆盖 communication patterns、data management、service decomposition 和 operations。第二版根据第一版之后的实践经验,对那些被证明存在问题的 patterns 做了显著更新。

Ford、Parsons 和 Kua 的 Building Evolutionary Architectures(第 2 版,O’Reilly,2022)引入 architectural fitness functions——对架构性质的自动化测试——并提供一个框架,用来设计可以演化而不积累债务的架构。对于需要把 architecture 当成持续工程,而不是一次性设计的团队,它尤其有价值。

书籍 作用 类型
Ford, Richards et al., Software Architecture: The Hard Parts 深入分布式架构决策 深入
Richards & Ford, Fundamentals of Software Architecture 易读的宽泛入口 入门
Evans, Domain-Driven Design DDD 奠基文本 深入
Vernon, Implementing Domain-Driven Design 面向实践者的 DDD 补充 深入
Fowler, Patterns of Enterprise Application Architecture 数据与 business logic patterns 深入
Martin, Clean Architecture Dependency management principles 深入
Newman, Building Microservices(第 2 版) Microservices 标准参考 参考
Ford, Parsons & Kua, Building Evolutionary Architectures(第 2 版) Evolutionary architecture practices 实践
Garlan & Shaw, Software Architecture 历史性奠基文本 深入
Gamma et al., Design Patterns GoF design patterns 参考
Brown & Wilson, The Architecture of Open Source Applications(免费;纸质版可付费购买) 真实开源架构案例研究 实践

课程、论文与当前资料

Martin Fowler 的 blog and website(martinfowler.com,免费)是软件架构领域持续价值最高的单一资源。关于 microservices、event sourcing、CQRS、strangler fig 和 architectural decision records 的文章具有权威性,并会定期更新。Bliki(blog + wiki)形式意味着文章会随着理解发展而修订;查看最后修订日期很有信息量。

Michael Nygard 的 original ADR proposal(cognitect.com,免费)是 Architectural Decision Records 的一手来源。MADR format(adr.github.io,免费)提供结构化模板。

ThoughtWorks Technology Radar(thoughtworks.com/radar,免费,每年发布两次)跟踪软件开发和架构中的新兴实践,并用 Adopt / Trial / Assess / Hold 做简短评估。连续阅读数年的 radar issues,可以看出哪些实践保持了持续兴趣,哪些只是短暂流行。

资源 平台 类型
Fowler, martinfowler.com(免费) martinfowler.com 参考
ThoughtWorks Technology Radar(免费) thoughtworks.com/radar 参考
Nygard, original ADR proposal(免费) cognitect.com 参考
MADR format(免费) adr.github.io 参考
AWS Well-Architected Framework(免费) docs.aws.amazon.com 参考
Azure Architecture Center / Cloud Design Patterns(免费) learn.microsoft.com 参考

实践、工具与当前资料

C4 model(c4model.com,免费,Simon Brown)提供了一种软件架构图的层级表示法:Context(系统如何嵌入其环境)、Containers(主要可部署组件)、Components(container 内的主要 modules),以及 Code(实现细节)。这个层级允许为不同受众提供合适抽象层级的 diagrams。配套 Structurizr tooling 可以从 DSL 生成 C4 diagrams,使 diagrams 与 codebase 保持同步。

ArchUnit(Java,免费,archunit.org)和 Dependency Cruiser(JavaScript,免费,GitHub)是把架构约束强制为自动化测试的工具:“这一层不应依赖那一层”“这些 packages 不应有 circular dependencies”“这个 module 只能依赖这份列表中的 modules”。把 architectural rules 编码成 executable tests,可以让 violations 在 CI 中暴露,而不是等到 code review 才发现。

arc42 template(免费,arc42.org)为架构文档提供结构——context、constraints、solution strategy、building block view、runtime view、deployment view、quality scenarios——确保重要架构方面被考虑并记录。

资源 平台 类型
C4 model + Structurizr(免费) c4model.com 实践
ArchUnit(Java,免费) archunit.org 实践
Dependency Cruiser(JS,免费) GitHub 实践
arc42 template(免费) arc42.org 参考

陷阱

陷阱 为什么会误导 更好的回应
Cargo-culting architectural styles Microservices、hexagonal architecture、event sourcing——每一个都是针对特定问题的解决方案,而不是普遍适用。把 microservices 应用到一个小团队构建和部署的系统上,会增加分布式系统复杂性,却没有获得证明这种复杂性合理的团队协调收益。把 event sourcing 应用到 CRUD application 上,会增加运维复杂性,却没有获得 auditability 或 temporal query 这些能证明其合理的收益。 采用某种 architectural style 之前,先说明它解决的具体问题,并验证该问题确实存在于当前系统中。Evans 的 DDD strategic patterns 适合 domain complexity 确实很高的情况;对简单 CRUD applications,它们只会增加 ceremony 而没有收益。采用 pattern 的决定,应由它处理的具体问题来证明。
把 architectural diagrams 当成 architecture Architecture 是一组 decisions 及其 rationale;diagrams 只是沟通其中一部分 decisions 的方式。一个画得漂亮、完整,却与真实构建方式不同的 UML diagram 没有任何用处;一个简单但截至上周仍然准确的数据流 sketch,则非常有价值。 保持架构文档最小、准确、当前。ADRs 记录 decisions;合适抽象层级上的 C4 diagrams 沟通结构。抵制创建全面文档的冲动,因为它们会很快过时;投资那些用最少变化表达最多决策信息的文档。
为预期需求设计 人们容易为系统当前没有、但未来可能有的需求进行设计;这会产生复杂度超过当前需求所能证明合理性的架构,并为可能永远不会实现的未来做优化。这就是 speculative generalization。 面向当前需求设计,同时明确考虑哪些 boundaries 需要保持可演化。Strangler fig pattern、记录哪些 decisions 是为了保留 optionality 的 ADRs,以及验证 architecture flexibility 的 architectural fitness functions,是在不为预期未来过度工程化的情况下管理演化的工具。
忽视组织结构 Conway’s Law——“设计系统的组织,受其 communication structure 约束,只能产生该组织 communication structure 的复制品”——已经得到经验验证。一个按 frontend/backend/database 组织的团队所设计的系统,会以这三层作为主要结构,无论 architect 在文档中怎样规定。 把组织结构当作架构输入。Inverse Conway Maneuver——设计团队结构以产生期望系统架构——是架构变革工具。Domain-Driven Design 的 Bounded Contexts 与 team ownership 对齐;让团队边界匹配期望 service boundaries,会产生由激励一致的团队维护的架构。
跳过 dependency analysis Dependency 是变化传播的机制;architecture 是管理 dependencies 的纪律。如果实践者实现架构时不显式分析 dependency graph——哪些 components 依赖哪些,方向如何,它们变化频率如何——就无法预测变化会在哪里变得昂贵。 为你负责的任何系统绘制 dependency graph,尽可能使用工具(ArchUnit、Dependency Cruiser)。识别被依赖最多的 components,并验证它们也是最稳定的。识别 circular dependencies 并解决它们。Dependency map 比任何展示架构“应该是什么”的 diagram 更能说明架构健康状况。