Part 1. Foundations
Chapter 2. Architectural Thinking
Architecture versus design
The traditional view draws a hard line between architect and developer, with a one-way flow of decisions across that line — and that one-way arrow is the source of most problems. Decisions the architect makes don’t reach developers; decisions developers make that change the architecture never get back to the architect. The architect ends up disconnected, and the architecture stops delivering what it set out to.
The fix is to break down the physical and virtual barriers and form a strong bidirectional relationship — architect and developers on the same virtual team, which also lets the architect mentor and coach. Modern architecture changes every iteration, so this synchronisation is essential. Where does architecture end and design begin? It doesn’t — they’re part of the same circle of life and must stay in sync.
Technical breadth
Developers need depth; architects need breadth. Picture a pyramid: “stuff you know” (depth, the peak) versus “stuff you know about but not deeply” (breadth, the middle). For an architect the wise move is to sacrifice some hard-won depth to broaden the portfolio, since the job is matching capabilities to constraints across a wide field. Some depth remains in enjoyable areas; the rest usefully atrophies.
Two common dysfunctions when developers transition to the role:
- Trying to maintain expertise across too many areas — succeeding at none and burning out.
- Stale expertise — the mistaken sense that your outdated knowledge is still cutting edge. Common in founders-turned-leaders who still make decisions on ancient criteria (the “Frozen Caveman” anti-pattern).
Analysing trade-offs
Architecture is the stuff you can’t Google. — Mark Richards
There are no right or wrong answers in architecture — only trade-offs. — Neal Ford
Worked example: an auction system where a Bid Producer must send each bid to Bid Capture, Bid Tracking, and Bid Analytics. Use a topic (pub/sub) or queues (point-to-point)?
- Topic advantages: architectural extensibility (new consumers added easily) and decoupling (the producer knows nothing about consumers).
- Topic disadvantages: data access/security (anyone can subscribe to the bid data), no heterogeneous contracts (all consumers share one contract), and harder monitoring/programmatic scalability.
Everything has an advantage and a disadvantage. Thinking like an architect is analysing them and asking “which matters more here — extensibility or security?” — the answer always depends on business drivers and context.
Balancing architecture and hands-on coding
Every architect should code and keep some technical depth, but avoid the bottleneck trap — owning critical-path/framework code so that you become a blocker.
Ways to stay hands-on without becoming a bottleneck:
- Delegate the critical-path/framework code to the team; code a piece of business functionality one to three iterations ahead instead. (You gain real experience, the team owns the hard parts, and you feel the same pain they do with process and tooling.)
- Do frequent proof-of-concepts to validate decisions against implementation reality — and write production-quality code even for throwaway POCs, since they often become the reference example others copy.
- Pick up technical-debt or architecture stories (low priority, low risk if unfinished).
- Work on bug fixes to find weak spots in the code and architecture.
- Build small CLI tools, analysers, and automated validators for the team.
- Write architectural fitness functions (e.g. ArchUnit) to automate compliance (Chapter 6).
- Do frequent code reviews — involvement in the code, compliance checking, and mentoring all at once.
Chapter 3. Modularity
Modularity is an organising principle. Software models complex systems, which tend toward entropy; architects must constantly expend energy to keep good structure, because it won’t happen by accident. It’s an implicit characteristic — no requirement ever asks for it, yet sustainable code bases depend on it.
Definition: a logical grouping of related code (a package in Java, namespace in .NET, etc.). Used loosely in the book for any related grouping — classes, functions, or otherwise. How developers package things matters architecturally: tightly coupled packages are hard to reuse independently.
Measuring modularity
Cohesion
How much the parts of a module belong together. A cohesive module is one where splitting it would just force the parts to call each other across the new boundary.
Attempting to divide a cohesive module would only result in increased coupling and decreased readability. — Larry Constantine
Cohesion measures, best to worst:
- Functional — every part relates to the others; the module is self-contained.
- Sequential — one module’s output is the next’s input.
- Communicational — modules form a chain, each operating on shared information (e.g. add a DB record, then email based on it).
- Procedural — modules must execute in a particular order.
- Temporal — related only by timing (e.g. things initialised together at startup).
- Logical — related logically but not functionally (e.g. a
StringUtilsgrab-bag of static methods). - Coincidental — related only by living in the same file; the worst kind.
Cohesion is less precise than coupling and often a judgement call (should “get/cancel customer orders” live in Customer Maintenance or a separate Order Maintenance module? It depends on how the two domains will grow and couple).
The Chidamber & Kemerer LCOM (Lack of Cohesion in Methods) metric structurally measures lack of cohesion — roughly, the sum of method-sets that don’t share fields. A class whose methods split cleanly into groups that touch disjoint fields scores high LCOM (those groups could be separate classes). It exposes incidental coupling within classes.
Coupling
- Afferent coupling — incoming connections to a code artifact.
- Efferent coupling — outgoing connections to other artifacts.
Abstractness, instability, and distance from the main sequence
- Abstractness = ratio of abstract artifacts (interfaces, abstract classes) to concrete ones. A 5,000-line
main()has abstractness near 0; an over-abstracted code base (theAbstractSingletonProxyFactoryBeanproblem) sits at the other extreme. - Instability = efferent / (efferent + afferent) coupling. Measures volatility — highly unstable code breaks more easily when changed, because a class delegating to many others is exposed to all their changes.
- Distance from the main sequence — a derived metric combining abstractness (A) and instability (I). Both fall between 0 and 1; the “main sequence” is the idealised line representing a healthy balance. Plotting a class and measuring its distance from that line shows how well-balanced it is:
- Upper-right corner = zone of uselessness (too abstract to use).
- Lower-left corner = zone of pain (too much implementation, brittle and hard to maintain).
On metric limits: even structural metrics need interpretation. Cyclomatic complexity measures complexity but can’t tell essential complexity (the problem is genuinely hard) from accidental complexity (the code is needlessly complex).
Connascence
Two components are connascent if a change in one would require the other to be modified to maintain overall correctness. — Meilir Page-Jones
Static connascence (source-level coupling), best to worst:
- of Name (CoN) — agree on a name. Most common and most desirable (rename tools make it trivial).
- of Type (CoT) — agree on a type.
- of Meaning / Convention (CoM/CoC) — agree on the meaning of values. Classic smell: hard-coded numbers instead of constants (imagine someone flipping
TRUE = 1/FALSE = 0). - of Position (CoP) — agree on the order of values. E.g.
updateSeat("14D", "Ford, N")is wrong even though the types match. - of Algorithm (CoA) — agree on an algorithm (e.g. a hash that must match on client and server).
Dynamic connascence (runtime):
- of Execution (CoE) — order of execution matters (setting email properties after
send()). - of Timing (CoT) — timing matters (race conditions).
- of Values (CoV) — interdependent values must change together (the four corners of a rectangle; or a single value updated across separate databases in a distributed transaction).
- of Identity (CoI) — components must reference the same entity (e.g. a shared distributed queue).
Dynamic connascence is harder to detect — we lack tools to analyse runtime calls as well as the static call graph.
Properties (how to use connascence well):
- Strength — measured by how easily the coupling can be refactored. Prefer static over dynamic; refactor toward weaker forms (e.g. CoM → CoN by introducing a named constant).
- Locality — how close the modules are. Stronger forms are tolerable when proximal (same module) and smellier when spread apart. Weigh strength and locality together.
- Degree — size of impact (how many classes). Small problems grow as code bases grow.
Page-Jones’s guidelines: minimise overall connascence by encapsulating; minimise what crosses encapsulation boundaries; maximise connascence within boundaries. Jim Weirich’s two rules — Rule of Degree (convert strong forms to weak) and Rule of Locality (the farther apart, the weaker the connascence should be).
Limits for architects: these metrics live at a low, code-hygiene level. Architects care more about how modules are coupled (synchronous vs asynchronous) than the raw degree, and connascence doesn’t address the sync/async question that dominates distributed-architecture design.
Chapter 4. Architecture Characteristics Defined
A software solution is domain (business) requirements plus architecture characteristics — the concerns about how the software is built and run. Domain requirements are agreed collaboratively; characteristics are squarely the architect’s responsibility. Also called non-functional or (at ThoughtWorks) cross-functional requirements.
A characteristic meets three criteria:
- Specifies a non-domain design consideration — operational/design success criteria, not “what the app does” (e.g. performance, or “prevent technical debt”).
- Influences a structural aspect of the design — it only rises to a characteristic when it needs special structure. Security illustrates this: a third-party payment processor needs only standard security hygiene (no special structure), whereas in-application payment processing may warrant an isolated module/service.
- Is critical or important to success — every characteristic adds complexity, so choose the fewest, not the most.
Characteristics are also implicit (rarely in requirements but needed anyway — availability, reliability, security; or low latency in a trading firm) or explicit (stated in requirements). They interact and trade off against each other — hence the constant use of the word trade-off.
A partial list
Operational: availability, continuity (disaster recovery), performance, recoverability, reliability/safety, robustness, scalability.
Structural: configurability, extensibility, installability, leverageability/reuse, localization, maintainability, portability, upgradeability.
Cross-cutting: accessibility, archivability, authentication, authorization, legal, privacy, security, supportability, usability.
Trade-offs and “least worst”
You can only support a few characteristics: each takes design effort, and improving one often hurts another (better security usually means more on-the-fly encryption and indirection, degrading performance). Architects rarely get to maximise everything — it comes down to trade-offs between competing concerns. The remedy is to make the architecture as iterative as possible, so you can change it as understanding improves rather than needing to nail it first time.
Chapter 5. Identifying Architectural Characteristics
When defining the driving characteristics with stakeholders, keep the final list as short as possible — and have stakeholders pick their top three (in any order), since they’ll rarely agree on a full priority ordering.
A common anti-pattern is the generic architecture that tries to support all characteristics — each one adds complexity before you’ve even touched the problem domain. Optimise for keeping the design simple, not for a particular characteristic count.
Translating business-speak to architecture
| Domain concern | Characteristics |
|---|---|
| Mergers & acquisitions | Interoperability, scalability, adaptability, extensibility |
| Time to market | Agility, testability, deployability |
| User satisfaction | Performance, availability, fault tolerance, testability, deployability, agility, security |
| Competitive advantage | Agility, testability, deployability, scalability, availability, fault tolerance |
| Time and budget | Simplicity, feasibility |
Worked kata — “Silicon Sandwiches”
A national sandwich shop wants online ordering. Thousands of users, perhaps millions one day. Requirements include pickup time + directions (integrating external mapping/traffic services), optional delivery dispatch, mobile accessibility, national and local daily promotions, and payment online/in-person/on-delivery. Context: franchised shops with different owners, near-future overseas expansion, and a corporate goal of cheap labour to maximise profit.
Explicit characteristics — derive from the requirements. “Thousands, perhaps millions” of users points to scalability (handle many concurrent users without serious degradation), even though scalability was never asked for directly — architects decode domain language into engineering terms. It also implies elasticity (handle bursts), which is different: a hotel reservation system is scalable but fairly steady, whereas a concert ticket system needs to absorb spikes. A sandwich shop has mealtime bursts, so elasticity lurks in the domain even though it isn’t stated.
Going requirement by requirement surfaces:
- External mapping services → integration points affecting reliability (a full mapping outage may be a deal-breaker unless you can switch providers; losing just traffic data could degrade gracefully with less accurate estimates).
- Mobile accessibility → native apps are expensive and clash with the “cheap labour / maximise profit” goal; maybe unrealistic.
- National/local promotions → scalability/elasticity for bursts, plus customisability (customisable per location).
- Payments → security, but with a third-party processor, standard hygiene suffices (no special structure).
- Franchised shops → cost constraints; check feasibility; a simple or sacrificial architecture may be warranted.
- Overseas expansion → internationalisation (i18n) — affects design, not structure.
- Cheap-labour goal → usability matters — affects design, not structure.
Implicit characteristics: availability (users can reach the site), reliability (it stays up mid-interaction), and security (implicit everywhere; prioritised by criticality). Security only becomes an architecture characteristic if it influences structure and is critical to the app.
There’s no best design, only a least-worst set of trade-offs. A useful exercise after a first pass: find the least important characteristic — which would you drop? For Silicon Sandwiches you could probably drop customisability (still implementable in app design) or performance (you don’t make it terrible, you just don’t prioritise it over scalability/availability).
Chapter 6. Measuring and Governing Architecture Characteristics
Measuring characteristics
Common problems with how organisations define characteristics:
- Vague meanings — many are hard to pin to a precise definition.
- Wildly varying definitions — even within one org, no agreement on something like performance; you need a common definition to have a productive conversation.
- Too composite — many decompose into others (agility = modularity + deployability + testability).
The remedy is a ubiquitous language for architecture — shared, objective definitions.
- Operational measures — performance/scalability are measurable but nuanced. Averaging response times can hide a slow 1% of requests, so also measure maximums. High-performing teams set targets from statistical models and alarm when real-time metrics fall outside the prediction (a deviation means either the model is wrong or something is wrong — both worth knowing).
- Structural measures — harder. Cyclomatic complexity addresses some of it; overly complex code is a universally agreed smell that harms nearly every desirable characteristic.
Open question: does cyclomatic complexity hold up in a Philosophy of Software Design world that prefers fewer, larger methods? Would that code base report higher overall complexity than the same logic split into many small methods?
- Some characteristics intersect with process (agility decomposes into testability/deployability). Prioritising ease of deployment and testability pushes the architect toward good modularity and isolation — a characteristic driving a structural decision.
Governance and fitness functions
The “evolutionary” in Building Evolutionary Architectures comes from evolutionary computing. A genetic algorithm is guided toward a goal by a fitness function — an objective measure of how close the output is to the aim (e.g. for the travelling-salesperson problem, route length, cost, or total travel time).
Architecture fitness function: any mechanism that provides an objective integrity assessment of some architecture characteristic (or combination).
Fitness functions aren’t a new framework — they’re a new lens on existing tools: metrics, monitors, unit-test libraries, chaos engineering, and so on.
Examples:
- Cyclic dependencies — auto-imports in Java/.NET quietly create dependency cycles, pushing toward a Big Ball of Mud (a component can’t be reused without dragging its friends along). Code reviews catch this too late; instead write a fitness function (e.g. using JDepend) that fails if cycles exist between packages.
- Distance from the main sequence — a JDepend-based test that fails any package falling outside a tolerance around the ideal line. A good example of an objective measure and of why developers and architects should build these together (not architects in an ivory tower writing esoteric checks developers can’t follow).
- Layer governance — use ArchUnit (Java) or NetArchTest (.NET) to enforce that, say, the presentation layer can’t depend on the persistence layer, preserving the layer boundaries the architect defined.
- Netflix’s Chaos / Simian Army — the Conformity Monkey enforces governance rules in production (e.g. every service responds to all RESTful verbs), the Security Monkey checks for known defects (open ports, misconfig), and the Janitor Monkey finds and removes orphaned services that nothing routes to anymore (since cloud services cost money).
Chapter 7. Scope of Architecture Characteristics
When systems were monolithic, characteristics applied to the whole system (“the system is scalable”). In distributed systems we need to scope characteristics at the component level (“this component is scalable”). The low-level structural metrics can’t measure how dependent components hang together — so we need the architecture quantum, which builds on connascence.
Coupling and connascence (revisited)
Two new categories for the distributed world:
- Static connascence — discoverable via static analysis (e.g. two microservices sharing a class definition like
Addressare statically connascent; changing it changes both). - Dynamic connascence — split into synchronous (caller waits for callee) and asynchronous (fire-and-forget, letting services differ in operational characteristics).
Architecture quanta and granularity
Architecture quantum: an independently deployable artifact with high functional cohesion and synchronous connascence.
- Independently deployable — includes everything needed to function alone. A system on one shared database is a quantum of one; microservices (each with its own database, per its bounded context) create multiple quanta.
- High functional cohesion — the quantum does something purposeful. In microservices, each service typically matches a single workflow / bounded context.
- Synchronous connascence — synchronous calls within or between the services forming the quantum. If a caller is far more scalable than its synchronous callee, you get timeouts and reliability problems; for the duration of the call their operational characteristics must align. An asynchronous link (e.g. a queue buffering an
Auctionservice feeding a slowerPaymentservice) creates a more flexible architecture.
The quantum is the new scope for characteristics. Defining them per-quantum rather than per-system surfaces challenges early and often leads to hybrid architectures.
Kata — “Going, Going, Gone” (online nationwide auctions; real-time bidding; auto-charge on win; reputation tracking; live video stream; ordered bid receipt). The authors identify three quanta with distinct characteristics:
- Bidder feedback (bid + video stream): availability, scalability, performance.
- Auctioneer (the live auctioneer): availability, reliability, scalability, elasticity, performance, security.
- Bidder (online bidders): reliability, availability, scalability, elasticity.
So a quantum is an early form of domain identification — encapsulating needs from the requirements with boundaries where the domain and its requirements change.
Chapter 8. Component-Based Thinking
Component scope
A component is a language-specific grouping above the class/function level:
- Library — runs in the same memory as the caller, communicates via function calls, usually a compile-time dependency.
- Subsystem / layer — a deployable unit, e.g. an event processor.
- Service — runs in its own address space, communicates over the network (TCP/IP, REST, queues) — a stand-alone deployable unit in styles like microservices.
Nothing requires components — they’re just a useful level of modularity above the language’s lowest. A microservice may be too small to warrant any. Components are the fundamental modular building block, and one of an architect’s primary decisions is the top-level partitioning.
Architect role
The architect defines, refines, manages, and governs components (with input from analysts, SMEs, developers, QA, ops, enterprise architects). Architecture is largely independent of development process — the main exception is the engineering practices from Agile (deployment, automated governance). The component is usually the lowest level the architect interacts with directly. Identifying components is one of the first tasks on a project — but first you must choose how to partition.
Architecture partitioning
Two common top-level partitionings:
- Technical partitioning — organise by technical capability (presentation, business, persistence). Organising principle: separation of technical concerns. Decouples layers (a persistence change only ripples to adjacent layers), and lets developers find code by capability. But domains get smeared across layers —
CatalogCheckoutappears in every layer. Aligns with the layered style (Chapter 10). Conway’s law makes this map onto siloed teams (backend, DBAs, presentation). - Domain partitioning — organise by domain/workflow, inspired by Eric Evans’s Domain-Driven Design. Each domain (e.g.
CatalogCheckout) is a top-level component that may contain layers internally. Aligns with the modular monolith and microservices.
Neither is more correct (First Law), but the industry has trended toward domain partitioning for both monolithic and distributed architectures.
Silicon Sandwiches partitioning — a domain partition gives components like Purchase, Promotion, MakeOrder, ManageInventory, Recipes, Delivery, Location (each with a customisation subcomponent for common/local variation). An alternative technical partition isolates Common and Local as top-level components.
- Domain partitioning — advantages: models the business rather than an implementation detail; eases the Inverse Conway Maneuver (cross-functional teams per domain); aligns with modular monolith / microservices; message flow matches the domain; easy to migrate to distributed. Disadvantage: customisation code appears in multiple places.
- Technical partitioning — advantages: clearly separates customisation code; aligns with the layered style. Disadvantages: high global coupling (changes to
Common/Localripple everywhere); duplicated domain concepts; higher data-level coupling (typically a single tangled database, hard to split later).These are heavy disadvantages — technical partitioning seems hard to justify here, and the common/local split only makes sense if there’s genuinely shared behaviour across local recipes/inventory/promotion/location.
Developer role
Developers subdivide components into classes, functions, or subcomponents. Class/function design is shared among architects, tech leads, and developers, mostly the latter.
Developers should treat an architect’s component design as a first draft, not the last word — implementation always reveals refinements.
Component identification flow
An iterative loop:
- Identify initial components — based on the chosen partitioning; treat as a first draft (odds of nailing it first try are near zero).
- Assign requirements to components to test the fit — may split, merge, or create components. Aim for a good coarse-grained substrate, not exactness.
- Analyse roles and responsibilities — align component and domain granularity to the roles and behaviours the app must support. Finding the right granularity is one of the hardest tasks.
- Analyse architecture characteristics — e.g. a part handling hundreds of concurrent users needs different characteristics than one handling a few, so a single “user interaction” component may need subdividing.
- Restructure — keep iterating with developers; you can’t anticipate all the edge cases, and understanding deepens as you build.
Granularity and discovery
Granularity is one of the hardest calls: too fine → excessive inter-component communication; too coarse → high internal coupling, hurting deployability, testability, and modularity.
Entity trap (anti-pattern) — making a Manager component per database entity. That’s not an architecture, it’s an ORM of a framework to a database; it signals no thought about the actual workflows, and yields components too coarse to guide developers.
Discovery techniques:
- Actor/Actions — identify actors and the actions they perform. Works well when requirements specify distinct roles and actions.
- Event storming — assume the system communicates via events/messages; find the events and build components around their handlers. Works well in distributed/microservices systems.
- Workflow approach — a more generic alternative for non-DDD/non-messaging contexts; identify roles, their workflows, and build components around the activities.
GGG — discovering components (using Actor/Actions): roles are bidder, auctioneer, and system (internal actions). From their actions come components: VideoStreamer, BidStreamer (read-only views), BidCapture, BidTracker (system of record), AuctionSession (start/stop + payment/resolution), Payment (third-party). Analysing characteristics then splits BidCapture into BidCapture and Auctioneer Capture, because the auctioneer needs higher reliability and availability (and less scale) than thousands of bidders — a dropped auctioneer connection is disastrous, a dropped bidder connection merely bad. BidTracker then unifies the single auctioneer stream with the many bidder streams.
Don’t obsess over the one true design — many will suffice (and are less likely to be over-engineered). Assess trade-offs objectively and pick the least-worst.
Monolith vs distributed (quantum redux)
If the system can live with a single quantum (one set of characteristics), a monolith has many advantages. Differing characteristics across components — as in GGG, where read-only streaming mixes badly with high-scale updates, and bidder differs from auctioneer — push you toward a distributed architecture.
Source
Mark Richards & Neal Ford, Fundamentals of Software Architecture, Part 1 (Chapters 2–8).