Components form the fundamental modular building block in architecture, which makes them a critical consideration for architects. An architect defines, refines, manages and governs components within an architecture — and not only components, but also connectors, shaping the interaction space and the media among components.
The division of labour matters. Developers typically take components, jointly designed with the architect, and further subdivide them into classes, functions or subcomponents. Class and function design is a shared responsibility of architects, tech leads and developers, with the lion’s share going to developers. And, in the module’s own words, developers should never take components designed by architects as the last word: all software design benefits from iteration, so that initial design should be viewed as a first draft, where implementation will reveal more details and refinements.
Before an architect can identify any component, one primary decision must be made: the top-level partitioning of components. You cannot identify components before knowing how you intend to partition the architecture.
There are two organising principles, and they are the fork in the road for the whole rest of the course.
| Technical partitioning | Domain partitioning | |
|---|---|---|
| Organising principle | Separation of technical concerns | Domain modelling — from Domain-Driven Design |
| Typical style | Layered monolith | Modular monolith, microservices |
| Top-level components | Presentation, business rules, services, persistence … | Domain capabilities or bounded contexts, e.g. CatalogCheckout |
| Status | Very common, predominant approach for decades | Widespread diffusion in recent years |
Note the important detail about domain partitioning: each of the components in the domain partitioning — for example CatalogCheckout — may still use a persistence library and have a separate layer for business rules. Layers do not disappear; they simply stop being the top-level organising principle.
CatalogCheckout workflow: on the left it appears in every layer, on the right it lives in one place.The choice of top-level partitioning is not only technical: there is a relationship between it and how the organisation, and the organisation of work, is structured.
“Organizations which design systems … are constrained to produce designs which are copies of the communication structures of these organizations.”
When a group of people designs a technical artefact, the communication structures between the people end up replicated in the design. People at all levels of organisations see this law in action and sometimes make decisions based on it. For example, it is common for organisations to partition workers based on technical capabilities — which makes sense from a purely organisational point of view, but hampers collaboration because of the artificial separation of common concerns.
The two partitionings therefore imply two organisational shapes:
Related observation by Jonny Leroy (ThoughtWorks): evolve team and organisational structure together to promote the desired architecture. If Conway’s law says the org shape leaks into the system shape, then design the org shape you want the system to have. Chapter 11 will restate this as the concrete microservices practice: “design your organization so that its structure mirrors your microservice architecture”.
Technical partitioning is not a mistake; it buys something real. It gives effective levels of technical decoupling: if the service layer is only connected to the persistence layer below and the business rules layer above, then changes in persistence will only potentially affect those layers. The separation also enables developers to find certain categories of the code base quickly, because the code is organised by capability.
The problem is that most realistic software systems require workflows that cut across technical capabilities. Take CatalogCheckout as a common business workflow: in a technically layered architecture the code that handles it appears in all the layers — the domain is smeared across the technical layers. With domain partitioning, changes are more localised.
Be able to state both sides. Technical partitioning → decoupling of technical concerns, easy to locate code by capability, org structured by specialism. Domain partitioning → changes localised to the domain that changed, org structured as cross-functional teams owning a component end to end. The deciding question is: what kind of change do you expect most often — a change of technology, or a change of business rule?
How is domain partitioning implemented inside a component? The course’s answer is the Clean Architecture, introduced by Robert C. Martin (“Uncle Bob”) to capture the separation of concerns proposed by different architectures over the last decades:
They all achieve the separation by dividing the software into layers: at least one layer for business rules, and another layer for user and system interfaces. And they share three characteristics:
| Characteristic | What it means |
|---|---|
| Testable | The business rules can be tested without the UI, database, web server, or any other external element |
| Independent of the UI | The UI can change easily without changing the rest of the system — a web UI could be replaced with a console UI without changing the business rules |
| Independent of the database | You can swap Oracle or SQL Server for Mongo, BigTable, CouchDB or something else: your business rules are not bound to the database |
Remember the hexagonal name: in Part III you will meet it again as the internal architecture of a single microservice — inbound adapters, business logic, outbound adapters. The FTGO monolith of Chapter 9 is itself described as a hexagonal architecture that happens to be one single deployment unit.
The concentric circles represent different areas of software. The further in you go, the higher level the software becomes. The outer circles are mechanisms; the inner circles are policies.
Source code dependencies must point only inward, toward higher-level policies.
Nothing in an inner circle can know anything at all about something in an outer circle. In particular, the name of something declared in an outer circle must not be mentioned by the code in an inner circle — that includes functions, classes, variables, or any other named software entity.
By the same token, data formats declared in an outer circle should not be used by an inner circle — especially if those formats are generated by a framework in an outer circle. We do not want anything in an outer circle to impact the inner circles.
There is no rule that says you must always have exactly four circles; there can be more. But the Dependency Rule always applies: source code dependencies always point inward; as you move inward the level of abstraction and policy increases; the outermost circle consists of low-level concrete details. So as you move inward the software grows more abstract and encapsulates higher-level policies, and the innermost circle is the most general and highest level.
Entities encapsulate enterprise-wide Critical Business Objects and Rules. An entity can be an object with methods, or a set of data structures and functions — it does not matter, so long as the entities can be used by many different applications in the enterprise.
If it is not an enterprise but just a single application, then these entities are the business objects of the application; they encapsulate the most general and high-level rules.
They are the least likely to change when something external changes. You would not expect these objects to be affected by a change to page navigation or security; no operational change to any particular application should affect the entity layer.
The use cases layer contains application-specific business rules. It encapsulates and implements all of the use cases of the system. These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their Critical Business Rules to achieve the goals of the use case.
Two expectations, in both directions:
A set of adapters that convert data from the format most convenient for the use cases and entities, to the format most convenient for some external agency such as the database or the web.
It is this layer that wholly contains the MVC architecture of a GUI: presenters, views and controllers all belong to the interface adapters layer. The models are likely just data structures passed from the controllers to the use cases, and then back from the use cases to the presenters and views.
Similarly, data is converted here from the form most convenient for entities and use cases to the form most convenient for whatever persistence framework is being used. No code inward of this circle should know anything at all about the database: if it is a SQL database, then all SQL should be restricted to this layer — and in particular to the parts of this layer that have to do with the database.
The outermost layer is generally composed of frameworks and tools such as the database and the web framework. Generally we do not write much code in this layer, other than glue code that communicates to the next circle inward.
This is where all the details go:
We keep these things on the outside, where they can do little harm.
Here is the apparent contradiction that the Dependency Rule creates. Consider the controllers and presenters communicating with the use cases in the next layer: the flow of control begins in the controller, moves through the use case, and then winds up executing in the presenter. But the flow of control goes in the opposite direction to the source code dependencies, each of which points inward toward the use cases.
The resolution is the Dependency Inversion Principle. In a language like Java we arrange interfaces and inheritance relationships such that the source code dependencies oppose the flow of control at just the right points across the boundary.
The canonical example from the module: suppose the use case needs to call the presenter. This call must not be direct, because that would violate the Dependency Rule — no name in an outer circle can be mentioned by an inner circle. So we have the use case call an interface (a use case output port) in the inner circle, and have the presenter in the outer circle implement it. The same technique is used to cross all boundaries: we take advantage of dynamic polymorphism to create source code dependencies that oppose the flow of control, so that we can conform to the Dependency Rule no matter which direction the flow of control travels.
Typically the data that crosses boundaries consists of simple data structures: basic structs or simple data transfer objects; or the data can simply be arguments in function calls; or you can pack it into a hashmap or construct it into an object.
The important thing is that isolated, simple data structures are passed across the boundaries. We do not want to cheat and pass Entity objects or database rows. Many database frameworks return a convenient data format in response to a query — call it a “row structure”. We do not want to pass that row structure inward across a boundary: doing so would violate the Dependency Rule, because it would force an inner circle to know something about an outer circle. When we pass data across a boundary, it is always in the form that is most convenient for the inner circle.
The module walks a request through a web-based Java system using a database. Follow it carefully — every step is either a data conversion or a boundary crossing.
Date objects, the Presenter will load the ViewModel with the corresponding Strings already formatted properly for the user. The same is true of Currency objects or any other business-related data. Button and MenuItem names are placed in the ViewModel, as are flags telling the View whether those buttons and menu items should be greyed out.All dependencies cross the boundary lines pointing inward, following the Dependency Rule.
Component identification works best as an iterative process, producing candidates and refinements through feedback. The module presents it as a cycle of five steps.
| Step | What happens |
|---|---|
| 1. Identify initial components | Before any code exists, the architect must somehow determine what top-level components to begin with, based on the chosen top-level partitioning. The module is candid: the likelihood of achieving a good design from this initial set of components is disparagingly small, which is precisely why architects must iterate. |
| 2. Assign requirements to components | Align requirements or user stories to those components to see how well they fit. This may entail creating new components, consolidating existing ones, or breaking components apart because they have too much responsibility. The mapping does not have to be exact: the architect is looking for a good coarse-grained substrate to allow further design and refinement. |
| 3. Analyse roles and responsibilities | While assigning stories, look at the roles and responsibilities elucidated during requirements to make sure the granularity matches. Thinking about both roles and behaviours lets the architect align component granularity with domain granularity. |
| 4. Analyse architecture characteristics | Look at the quality attributes discovered earlier to see how they impact component division and granularity. The module’s example: two parts of a system might both deal with user input, but the part dealing with hundreds of concurrent users needs different characteristics from one supporting only a few — so a purely functional view yielding a single “user interaction” component gets subdivided once characteristics are considered. |
| 5. Restructure components | Feedback is critical in software design, so architects must continually iterate on their component design with developers. Designing software provides all kinds of unexpected difficulties and no one can anticipate all the unknown issues; as architects and developers delve deeper into building the application, they gain a more nuanced understanding of where behaviour and roles should lie. |
Finding the proper granularity for components is one of an architect’s most difficult tasks:
The top-level partitioning: technical or domain. Components cannot be identified before you know how the architecture is partitioned, because the partitioning determines what kind of thing a top-level component is — a technical capability (presentation, business rules, services, persistence) or a domain capability / bounded context (Catalog, Checkout, Shipping).
No. Each component in a domain partitioning may still use a persistence library and have a separate layer for business rules. Layers move one level down: the top-level partitioning revolves around domains, and technical layering becomes an internal concern of each domain component.
Conway’s law: “Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations.” When people design an artefact, their communication structures end up replicated in the design. Inverse Conway Manoeuvre (Jonny Leroy, ThoughtWorks): evolve team and organisational structure together to promote the desired architecture — i.e. use the law deliberately, shaping the org to obtain the architecture you want.
It buys effective technical decoupling — if the service layer only connects to persistence below and business rules above, a persistence change potentially affects only those layers — and it makes categories of code easy to find. It costs locality of business change: most realistic systems have workflows that cut across technical capabilities, so a workflow such as CatalogCheckout appears in all layers and the domain is smeared across them.
Hexagonal Architecture / Ports and Adapters (Cockburn), DCI (Coplien and Reenskaug) and BCE (Jacobson). They all divide the software into layers with at least one layer for business rules and another for user and system interfaces, and they all yield systems that are testable (business rules testable without UI, database or web server), independent of the UI and independent of the database.
Source code dependencies must point only inward, toward higher-level policies. Nothing in an inner circle can know anything about something in an outer circle; the name of anything declared in an outer circle must not be mentioned by code in an inner circle — functions, classes, variables, any named entity. By the same token, data formats declared in an outer circle should not be used by an inner circle, especially formats generated by an outer-circle framework.
No — there is no rule saying you must always have just these four; there can be more. What always applies is the Dependency Rule: dependencies point inward, abstraction and policy increase as you move inward, and the outermost circle consists of low-level concrete details, with the innermost circle the most general and highest level.
With the Dependency Inversion Principle. The use case must not call the presenter directly, since that would name an outer-circle entity from the inner circle. Instead the use case calls an interface it owns — a use case output port declared in the inner circle — and the presenter in the outer circle implements that interface. Dynamic polymorphism makes the source dependency oppose the flow of control at exactly the right point. The same technique is used to cross every boundary.
Simple, isolated data structures: basic structs, simple DTOs, arguments in function calls, a hashmap, a constructed object. What must not cross is anything carrying an outer-circle dependency — you must not “cheat and pass Entity objects or database rows”. Passing a framework’s row structure inward would force the inner circle to know something about the outer circle. Data crossing a boundary is always in the form most convenient for the inner circle.
The OutputData is a plain object built by the UseCaseInteractor from the Entities; it may contain Date or Currency objects. The Presenter repackages it into the ViewModel, which contains mostly Strings and flags already formatted for the user, plus button and menu item names and greyed-out flags. This leaves the View with almost nothing to do but move data into the HTML page — and keeps all formatting policy out of both the View and the use case.
Because before any code exists the architect must guess top-level components from the chosen partitioning, and “the likelihood of achieving a good design from this initial set of components is disparagingly small”. The remedy is the iterative identification cycle: assign requirements, analyse roles and responsibilities, analyse architecture characteristics, restructure — and repeat, continually, with developers, because no one can anticipate the unknown issues that arise during a project.
Too fine-grained: too much communication between components is needed to achieve results. Too coarse-grained: high internal coupling, difficulties in deployability and testability, and other modularity-related negative side effects. Finding the proper granularity is described as one of an architect’s most difficult tasks.