English
Software architecture concerns the organization of a system: responsibilities, interfaces, data ownership, dependencies, and deployment units. It helps answer questions such as where a business rule belongs, which parts must change together, and what continues working when a dependency fails. A decision is architecturally significant because of its consequences in a particular system, not simply because it appears in a diagram or is difficult to reverse.
Consider a reservation application. Checking that a seat is available and reserving it must be coordinated to prevent two customers buying the same seat. Sending a confirmation email can happen later. Putting these operations in separate services changes how consistency and failure must be handled; putting them in separate modules inside one application changes how developers organize the code. A module boundary and a network boundary solve different problems.
The starting point is a set of concrete requirements. “Fast and scalable” is too vague to evaluate. “At 100 reservation requests per second, 95% finish within 300 milliseconds, and confirmed reservations survive a process restart” supplies a workload, measurements, and a recovery requirement. These are example targets, not universal standards. Architecture connects such requirements to a design and to evidence from tests, prototypes, and operation.
Prerequisites: Programming (§2.1) and experience changing a small application. Databases (§4.4) help with data ownership and transactions; distributed systems (§4.6) become important when boundaries cross machines. A single-process program is enough to begin studying modularity.
From Subroutines to Evolutionary Architecture
Large systems were deliberately organized before software architecture became a distinct research field. Subroutine libraries, operating systems, and business applications already required decisions about shared data and interfaces. The difficulties of projects such as OS/360 and Multics made a limitation clear: dividing work among more programmers does not by itself make the resulting pieces fit together.
Dijkstra’s 1968 account of the THE multiprogramming system showed how a hierarchy of abstractions could support construction and reasoning in stages. A higher level used capabilities established below it, rather than dealing with every hardware and scheduling detail at once. Layering was useful because it restricted what each part had to assume and helped organize verification; it was more than a drawing convention.
Parnas’s 1972 paper on modular decomposition asked which information a module should hide. His keyword-in-context indexing example contrasted dividing a program by processing steps with dividing it around design decisions, such as text storage and the representation of shifted lines. If several routines directly manipulate the same storage layout, changing that layout affects them all. An interface that hides the representation can localize the change, provided its promised behavior remains the same.
Structured design supplied the vocabulary of coupling and cohesion. Object-oriented languages supplied ways to combine state and operations, but objects and inheritance did not automatically produce good boundaries. The 1994 Design Patterns book documented recurring designs and their tradeoffs; UML later offered shared diagram notations. Patterns help describe an intended solution, while diagrams help inspect particular relationships. Neither establishes that the solution meets a system’s requirements.
Shaw and Garlan’s work in the 1990s helped systematize architectural styles, including pipes and filters, layers, and client-server systems. Enterprise application patterns and Evans’s Domain-Driven Design then developed ways to organize business rules and persistent state. A bounded context makes a model’s meaning explicit: a “customer” in billing may have different attributes and rules from a “customer” in support, even when both refer to the same person.
Microservices made independent deployment and service ownership prominent design concerns. They also made network failures, compatibility, and cross-service data changes part of ordinary application work. Later work on evolutionary architecture emphasized checking important properties as systems change. An automated rule that the pricing module must not import a database driver can preserve one boundary; it cannot establish that the pricing model itself is appropriate. Architectural practice combines such checks with continuing review of requirements and costs.
Coupling, Modularity, and Architectural Styles
The Organizing Principles
Coupling describes dependencies between parts; cohesion describes how well the responsibilities within a part belong together. These are related concerns, not the same quantity. Putting unrelated functions into one giant module can reduce the number of inter-module dependencies while making the module harder to understand. Creating an interface for every function can multiply coordination without hiding any meaningful decision.
Distinguish several dependencies. A source dependency appears when one module imports another. A runtime dependency exists when an operation needs another component to respond. A data dependency exists when components assume the same schema or interpretation. Two services with no shared code may still require simultaneous releases because one expects a newly renamed field. Counting imports alone misses this coupling.
Information hiding means keeping a changeable decision behind a contract. A storage interface might promise “return the reservation associated with this identifier” without exposing a table layout. The contract also needs relevant semantics: can a just-written reservation be read immediately, how is absence represented, and which failures can occur? An abstraction that hides the database name but exposes all its query conventions may offer little independence.
Dependency inversion concerns source dependencies. Reservation logic can define a ReservationStore interface; a SQL adapter implements it. The logic imports the interface, and the adapter depends on that same contract. Control can still flow from the logic into the adapter at runtime. This permits an in-memory implementation in some tests, but differences in transactions and concurrency still require integration tests with the real database.
For a proposed boundary, ask which changes it should contain and trace an example through the system. Changing the email provider should mostly affect an adapter; changing the meaning of a confirmed reservation may affect storage, APIs, and user messages. Not every widespread change indicates bad modularity: some requirements really change several contracts. Incoming and outgoing dependency counts help locate candidates for review, but do not measure change cost or semantic stability by themselves.
Architectural Styles and Their Tradeoffs
Layered architecture restricts dependencies among responsibilities such as presentation, application logic, and data access. A strict layering rule permits calls only to the next layer; other designs allow skipping layers or invert source dependencies through interfaces. Layers are logical divisions, while tiers are separately deployed parts. Three layers can run in one process. Evaluate a layering rule by the changes and reasoning it simplifies, rather than by the number of layers.
A modular monolith deploys as one application while maintaining internal boundaries. It can share a database transaction across operations and avoid network calls between modules. Its costs include a shared release unit and possibly shared resource limits. Microservices introduce separately deployable services, which can support different scaling needs or team release schedules. They do not require one repository or one physical database server per service; what matters is ownership and whether other services bypass the service’s contracts to manipulate its data.
Failure isolation must be designed. Suppose a request requires five services, each available with probability 0.999, and their availability is independent. The probability that all five are available is 0.999⁵ ≈ 0.995, about 99.5%. Splitting a function across services has not automatically improved its availability. Timeouts, bounded retries, resource isolation, and degraded responses can change the outcome; common dependencies also invalidate the independence assumption.
Events reduce the need for producers to address particular consumers, but retain dependencies on schemas, meanings, and delivery behavior. After confirming a reservation, publishing an event separately can fail between the database commit and the send. A transactional outbox stores the reservation change and a pending event in one database transaction; a separate publisher sends the event. A retry may publish it again, so consumers must handle duplicates. This solves a specific consistency gap rather than making the whole workflow one distributed transaction.
Choose a style from the dominant constraints. A small reservation application may benefit from modules and one transaction. A separately scaled reporting service may accept delayed updates. A business operation that cannot tolerate inconsistent intermediate states may belong within one transactional boundary. Service count, diagram complexity, and fashionable terminology are not measures of architectural quality.
Architectural Decision Records
An Architectural Decision Record (ADR) preserves the reasoning behind a consequential choice. Record the problem and constraints, alternatives, decision, expected consequences, and status. Identify assumptions that should trigger reconsideration. Nygard’s original proposal and the MADR templates offer starting formats; the purpose is to make the decision understandable later, not to complete a form mechanically.
For the reservation application, an ADR might choose one database transaction for seat allocation because preventing overselling matters more than deploying seat and reservation code independently. It can record the rejected split-service alternative, the expected workload, and the measurements that would justify revisiting the design. “We use a monolith because it is simpler” leaves out the actual consistency requirement and the costs being compared.
Attach evidence at the appropriate scale: a concurrency test for double booking, a load test for the stated workload, or an import rule for a module boundary. These checks test selected claims. If a decision changes, retain the old record with a superseded status and link the replacement, so that later readers can distinguish past assumptions from current ones.
What Studying This Changes
Studying architecture helps you trace the consequences of a change. You can ask which contracts are affected, where data must remain consistent, and what additional failure modes a network boundary introduces. A proposed design becomes a set of claims that can be examined rather than a list of named technologies.
A useful exercise is to take a small application and change its storage implementation without changing its public behavior. Record every caller that must change and explain why. Then add a second delivery mechanism, such as a command-line interface beside a web interface. The parts that can be reused reveal which responsibilities were actually separated.
Next, propose one service extraction without implementing it. Write down the expected benefit, data ownership, compatibility rules, failure behavior, and operational cost. If the benefit cannot be stated concretely, keeping the module inside the application is a reasonable current decision. Revisiting that decision when the workload or organization changes is part of architectural work.
Resources
Books and Texts
Richards and Ford provide an overview of architectural concerns; Evans develops domain modeling; Fowler examines enterprise application patterns. Read these against a small system whose requirements you understand. The Architecture of Open Source Applications, edited by Brown and Wilson, supplies concrete case studies. Building Microservices and Software Architecture: The Hard Parts are more useful once you can identify an actual distribution problem. The second edition of Building Evolutionary Architectures is by Ford, Parsons, Kua, and Sadalage.
| 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 & Sadalage, Building Evolutionary Architectures (2nd ed.) | Checking architectural properties during change | Practice |
| Shaw & Garlan, Software Architecture: Perspectives on an Emerging Discipline | Architectural styles and their historical formulation | 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
Use Mary Shaw’s KWIC teaching material to compare decompositions by attempting changes, and MADR to record a decision. Fowler’s articles clarify patterns and tradeoffs. Vendor architecture guides provide examples under the vendor’s service assumptions; compare the underlying requirement before adopting a recommendation.
| 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
Draw the context and main runtime interactions of a small application, then explain one failure path. C4 and Structurizr can help communicate views; ArchUnit or Dependency Cruiser can check selected code dependencies. Choose a check that protects an actual boundary rather than adding diagrams or rules merely to fill a template.
| Resource | Platform | Type |
|---|---|---|
| C4 model / Structurizr documentation | c4model.com / structurizr.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 |
|---|---|---|
| Choosing a style before stating requirements | A named style does not specify the workload, consistency needs, or operating cost. | Describe the scenario and compare concrete alternatives. |
| Counting interfaces as modularity | Interfaces can expose storage details or unstable semantics. | Identify the decision each interface hides and test a representative change. |
| Assuming events remove coupling | Consumers still depend on event meaning, compatibility, ordering, and duplicates. | Specify delivery and versioning rules and trace partial failures. |
| Assuming microservices isolate every failure | Required call chains and shared resources can spread failures. | Analyze request dependencies and design timeouts, isolation, and degraded behavior. |
| Treating diagrams as complete specifications | A box and arrow rarely explain consistency, ownership, or failure semantics. | Pair diagrams with contracts, decision records, and targeted checks. |
中文
软件架构研究系统怎样组织:各部分的职责、接口、数据归属、依赖关系和部署单位。它帮助回答业务规则应放在哪里、哪些部分必须一起修改、依赖发生故障后哪些功能还能运行等问题。一个决策是否具有架构意义,取决于它对具体系统的影响,而不只是它是否画在图上或难以撤销。
以预约应用为例。检查座位是否可用与完成预订必须协调,避免两个顾客买到同一个座位;发送确认邮件则可以稍后进行。把这些操作放进不同服务,会改变一致性和故障处理方式;把它们划分为同一应用内的不同模块,会改变代码的组织方式。模块边界和网络边界解决的是不同问题。
起点是一组具体要求。“快速、可扩展”很难检验。“每秒 100 个预约请求时,95% 在 300 毫秒内完成,且进程重启后已确认的预约仍然保留”,则给出了负载、测量方法和恢复要求。这是示例目标,不是通用标准。架构把这些要求与设计,以及测试、原型和实际运行提供的证据联系起来。
前置知识:编程(§2.1)及修改小型应用的经验。数据库(§4.4)有助于理解数据归属和事务;边界跨越机器时,需要分布式系统(§4.6)的知识。学习模块化可以从单进程程序开始。
从 Subroutines 到 Evolutionary Architecture
在软件架构成为独立研究领域之前,大型系统就已经需要有意识的组织。子程序库、操作系统和业务应用都涉及共享数据与接口的设计。OS/360、Multics 等项目的困难说明:把工作分给更多程序员,本身并不能让各部分正确配合。
Dijkstra 在 1968 年介绍 THE 多道程序系统时,展示了怎样利用抽象层次分阶段构建和推理。较高层使用下层已建立的能力,不必同时处理全部硬件和调度细节。分层的作用在于限制各部分需要依赖的假设,并帮助组织验证,而不只是统一画图方式。
Parnas 在 1972 年关于模块分解的论文中,追问模块应该隐藏什么信息。他用关键词上下文索引程序,对比按处理步骤分解与按设计决策分解,例如隐藏文本存储方式和移位行的表示。如果多个例程直接操作同一种存储布局,修改布局就会影响它们。通过接口隐藏表示,可以让修改集中在局部,前提是接口承诺的行为保持不变。
结构化设计提供了耦合与内聚的术语。面向对象语言提供了组合状态和操作的方法,但对象与继承不会自动产生合理边界。1994 年的《Design Patterns》记录了反复出现的设计及其权衡;UML 随后提供了共享的图示语言。模式帮助描述方案,图示帮助检查特定关系,两者都不能单独证明方案满足系统要求。
Shaw 和 Garlan 在 1990 年代的工作,帮助系统整理了管道与过滤器、分层、客户端与服务器等架构风格。企业应用模式与 Evans 的领域驱动设计进一步发展了组织业务规则和持久化状态的方法。限界上下文明确一个模型的含义:账务中的“客户”和客服中的“客户”可能有不同属性与规则,即使它们指向同一个人。
微服务使独立部署和服务归属成为突出的设计问题,也使网络故障、兼容性和跨服务数据变更进入日常应用开发。后来的演进式架构研究强调在系统变化时持续检查重要性质。例如,自动检查定价模块不得导入数据库驱动,可以维持一条依赖边界,却不能证明定价模型本身合理。架构实践需要结合这类检查,持续审视需求与成本。
Coupling、Modularity 与 Architectural Styles
组织原则
耦合描述部分之间的依赖,内聚描述一个部分内部的职责是否相互关联。两者有关,但不是同一个量。把无关功能都塞进一个大模块,可以减少模块之间的依赖数量,却让模块更难理解;给每个函数都加接口,也可能只增加协调工作,没有隐藏任何重要决策。
应区分几类依赖。模块导入另一个模块时,存在源码依赖;操作需要另一个组件响应时,存在运行时依赖;组件假设相同的数据结构或含义时,存在数据依赖。两个没有共享代码的服务,也可能因为字段改名而必须同时发布。只统计导入关系看不到这种耦合。
信息隐藏是把可能变化的决策放到契约之后。例如,存储接口可以承诺“返回此标识符对应的预约”,而不暴露表结构。契约还需要说明相关语义:刚写入的预约能否立即读到、如何表示不存在、可能出现哪些失败。如果抽象只隐藏数据库名称,却暴露其全部查询习惯,它提供的独立性可能很有限。
依赖倒置关注源码依赖。预约逻辑可以定义 ReservationStore 接口,由 SQL 适配器实现。逻辑导入接口,适配器也依赖该契约;运行时控制流仍然可以从逻辑进入适配器。这样可以在部分测试中使用内存实现,但事务和并发方面的差异,仍需通过真实数据库的集成测试检查。
评估边界时,要问它应该把哪些修改限制在内部,并追踪一个具体变化。更换邮件提供方应主要影响适配器;改变“预约已确认”的含义,则可能影响存储、API 和用户消息。修改范围大不一定意味着模块化失败,有些需求确实改变了多个契约。传入和传出的依赖数量可以帮助寻找审查对象,但不能单独衡量修改成本或语义稳定性。
Architectural Styles 及其权衡
分层架构限制展示、应用逻辑和数据访问等职责之间的依赖。严格分层只允许调用下一层;其他设计可能允许跨层,或用接口倒置源码依赖。层是逻辑划分,而部署层级指分别部署的部分;三个逻辑层可以运行在一个进程中。评价分层规则,要看它简化了哪些修改和推理,而不是层数多少。
模块化单体作为一个应用部署,同时保留内部边界。它可以让多个操作共享数据库事务,并避免模块间的网络调用;代价包括共享发布单位,以及可能共享资源限制。微服务引入可分别部署的服务,可以满足不同扩缩容需求或团队发布节奏。它不要求每个服务都有独立代码仓库或物理数据库服务器;重要的是数据归属,以及其他服务是否绕过契约直接操作其数据。
故障隔离需要专门设计。假设一个请求必须使用五个服务,每个服务可用的概率都是 0.999,且它们的可用性相互独立,那么五个都可用的概率是 0.999⁵ ≈ 0.995,约为 99.5%。把功能拆成服务,并没有自动提高可用性。超时、有界重试、资源隔离和降级响应可以改变结果;共享依赖也会使独立性假设失效。
事件减少了生产者直接指定消费者的需要,却保留了对结构、含义和投递行为的依赖。确认预约后,如果另行发布事件,就可能在数据库提交与发送之间失败。事务发件箱把预约变更和待发送事件放在同一个数据库事务中保存,再由独立发布程序发送。重试可能再次发布同一事件,因此消费者需要处理重复。它解决的是一个具体的一致性缺口,并没有让整个流程成为一个分布式事务。
应根据主要约束选择架构风格。小型预约应用可能适合模块划分和单个事务;单独扩容的报表服务可能接受延迟更新;不能容忍不一致中间状态的业务操作,则可能适合放在同一个事务边界内。服务数量、图示复杂程度和流行术语,都不是架构质量的尺度。
Architectural Decision Records
架构决策记录(ADR)保存重要选择背后的推理。记录问题与约束、备选方案、决定、预期后果和状态,并指出哪些假设变化后需要重新评估。Nygard 的原始提议和 MADR 模板可以作为起点;目的是让后来的人理解决定,而不是机械填表。
对于预约应用,一条 ADR 可以选择用一个数据库事务分配座位,因为防止超卖比独立部署座位代码与预约代码更重要。记录中可以说明被放弃的拆分服务方案、预期负载,以及哪些测量结果会促使团队重新考虑。只写“单体更简单”,会遗漏真正的一致性要求和比较的成本。
应为不同主张配上相应证据:用并发测试检查重复预订,用负载测试检查指定工作负载,用导入规则检查模块边界。这些检查只覆盖选定的主张。决策变化时,保留旧记录、标记为已替代并链接新记录,使后来的人能区分过去的假设与当前决定。
学习这一部分会改变什么
学习架构能够帮助你追踪修改的后果:哪些契约受到影响、哪些数据必须保持一致、网络边界增加了什么故障方式。设计方案因而成为一组可以检查的主张,而不是技术名称的清单。
一个有用的练习是:修改小型应用的存储实现,保持公开行为不变。记录哪些调用方必须修改,并解释原因。然后在 Web 界面之外增加命令行界面等另一种使用方式。哪些部分可以复用,会显示哪些职责确实被分开了。
接下来,先提出一个服务拆分方案,不急于实现。写清预期收益、数据归属、兼容规则、故障行为和运维成本。如果无法具体说明收益,把模块继续保留在应用内部就是合理的当前决定。负载或组织变化后重新评估,也是架构工作的一部分。
资源
书籍与文本
Richards 和 Ford 的书概述架构问题,Evans 讨论领域建模,Fowler 分析企业应用模式。可以结合一个需求已知的小系统阅读。Brown 和 Wilson 编辑的《The Architecture of Open Source Applications》提供具体案例。能够识别实际分布式问题后,再读《Building Microservices》和《Software Architecture: The Hard Parts》更容易理解其用途。《Building Evolutionary Architectures》第二版的作者包括 Ford、Parsons、Kua 和 Sadalage。
| 书籍 | 作用 | 类型 |
|---|---|---|
| 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、Sadalage,Building Evolutionary Architectures(第 2 版) | 在演化中检查架构性质 | 实践 |
| Shaw、Garlan,Software Architecture: Perspectives on an Emerging Discipline | 架构风格及其早期系统整理 | 深入 |
| Gamma et al., Design Patterns | GoF design patterns | 参考 |
| Brown & Wilson, The Architecture of Open Source Applications(免费;纸质版可付费购买) | 真实开源架构案例研究 | 实践 |
课程、论文与当前资料
可以用 Mary Shaw 的 KWIC 教学材料,通过实际修改比较模块划分,再用 MADR 记录决定。Fowler 的文章解释模式与权衡。厂商架构指南提供基于其服务假设的例子,采用建议前应先比较其基础要求。
| 资源 | 平台 | 类型 |
|---|---|---|
| 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 和 Structurizr 帮助表达不同视图;ArchUnit 或 Dependency Cruiser 可以检查特定源码依赖。选择能够保护实际边界的检查,不必为了填满模板而增加图示或规则。
| 资源 | 平台 | 类型 |
|---|---|---|
| C4 模型 / Structurizr 文档 | c4model.com / structurizr.com | 实践 |
| ArchUnit(Java,免费) | archunit.org | 实践 |
| Dependency Cruiser(JS,免费) | GitHub | 实践 |
| arc42 template(免费) | arc42.org | 参考 |
陷阱
| 误区 | 为什么会误导 | 更好的做法 |
|---|---|---|
| 先选风格再考虑要求 | 架构名称没有说明负载、一致性需求和运维成本。 | 先描述场景,再比较具体方案。 |
| 用接口数量衡量模块化 | 接口仍可能暴露存储细节或不稳定的语义。 | 指出每个接口隐藏的决策,并尝试一次有代表性的修改。 |
| 认为事件消除了耦合 | 消费者仍然依赖事件含义、兼容性、顺序和重复处理规则。 | 明确投递与版本规则,追踪部分失败。 |
| 认为微服务自动隔离所有故障 | 必需的调用链和共享资源可能传播故障。 | 分析请求依赖,设计超时、隔离和降级行为。 |
| 把图示当成完整规范 | 方框与箭头通常没有解释一致性、归属和故障语义。 | 用契约、决策记录和针对性检查补充图示。 |