Chapter Roadmap (1 of 3)
- 10.1 Why designing good classes is not enough
- Responsibilities exist at more than one scale.
- 10.2 Layers: giving each boundary a reason
- Eight packages, each with a reason.
- 10.3 What each layer does: the four layers in practice
- Where an outside collaborator sits.
- 10.4 Which way do dependencies point, and how do we check?
- Source dependencies, runtime calls and a compiled dependency check.
- 10.5 Cutting packages well: cohesion, coupling and stability
- CCP, CRP, REP, ADP, SDP, SAP and a worked placement comparison.
Chapter Roadmap (2 of 3)
- 10.6 Entities and value objects: identity or value?
- Identity, immutability and Java equality are three different things.
- 10.7 Repositories and services: a storage contract
- The smallest useful storage contract.
- 10.8 Recording a small architecture decision
- ADR-010.
- 10.9 Peer review and preparing Milestone 3
Chapter Roadmap (3 of 3)
- 10.10 Checking an AI’s dependency review verify a dependency review.
- 10.11 Structure habits that mislead
- 10.12 Try it yourself: restructure and check the dependencies
- The book’s project B1 to B6; your team’s project T1 to T5.
- 10.13 Questions to think with, and a transfer challenge
- A clinic appointment application.
- What to take away, and what comes next
- Chapter 11: refactoring and legacy code.
Learning Outcomes (1 of 2)
By the end of this chapter, you should be able to:
- Assign existing responsibilities to presentation, application, domain and infrastructure code.
- Distinguish runtime calls from source-code dependency direction.
- Explain entity identity, immutability and value equality without conflating them.
- Locate a focused repository contract and its implementation.
Learning Outcomes (2 of 2)
- Restructure a use case while checking its observable behaviour and compatibility limits.
- Record an architecture decision and prepare evidence for peer review and Milestone 3.
- Compare two package designs using cohesion, coupling and stability, and read dependency counts without treating them as a measure of quality.
Module Connection and Starting Point
- Supports LO2 and LO6; Milestone 3 preparation. Readings: Martin’s principles of package design and selected chapters of Evans’s domain-driven design.
- Starting point: Chapter 9’s tested Java 17 project, including the stronger assertions added when it was reviewed.
- The completed version preserves its use-case results and earlier demonstrations.
- Package names, imports, launcher names and direct coordinator construction change: not a claim of source or binary compatibility.
Milestone 3 Checklist (1 of 2)
Milestone 3 falls in this week: design patterns and layered architecture, with a demonstration and a retrospective. On your team’s project:
- A design pattern from Chapters 8 and 9 used where it removes a real problem, and one place you declined to use one.
- A compact package diagram saying which packages may depend on which.
- One restructuring change, with moves kept separate from logic changes.
- Passing checks, including a dependency check or a documented review of imports.
Milestone 3 Checklist (2 of 2)
- A short architecture decision record with alternatives, consequences and a revisit trigger.
- Real peer review of that change, with the feedback addressed.
- A demonstration and a retrospective with a next action.
- A statement of what the architecture does not do.
- Start with items 2 and 5, which need no code.
- The approved Milestone 3 instructions decide what is submitted. This list is preparation, not a rubric.
How to Use This Chapter
- Core reading: 10.1 to 10.5 (except the Ports and Adapters comparison), 10.7, 10.8, 10.9.
- Working knowledge: 10.6 (entities and value objects): understand identity versus value meaning, and why Java equality is a separate decision. You do not implement either.
- Minimum lab: B1 to B3 (the book’s project: reading, tracing, diagnosis; nothing to rebuild) and T1 to T5 (your team’s project; your Milestone 3 evidence).
- Optional: Ports and Adapters, the AI activity, B4 to B6 (break the rules on purpose; reproduce the migration), review questions, transfer.
Opening Problem: Understandable Classes, No Visible Structure
- The application now has borrowing policies, a directory adapter, notification collaborators, loan operations and several demonstrations.
- Individual classes have understandable responsibilities, but the production code still sits largely in Java’s unnamed package.
- Nothing tells a new developer whether a borrowing policy may depend on a directory client, or where a storage implementation should be constructed.
A Deliberately Modest Change
- Make those boundaries visible while preserving the demonstrated reservation behaviour.
- Decide which classes express domain rules, which coordinate, which communicate with external mechanisms, which assemble the application.
- Moving files alone will not answer those questions.
- The chapter adds a small reservation storage contract, moves concrete storage outside the coordinator, and documents dependency rules that can be checked against compiled code. No database, web framework or distributed system.
Class Correctness Is Not an Application Boundary
- A well-designed
LoanPeriodcan reject an invalid interval while the surrounding application has poor dependency organisation. - A correct directory adapter can be called directly from several user interfaces, each repeating eligibility logic differently.
- Chapters 5 to 9 separated several responsibilities. Now we make the distinctions visible across packages.
Layering
- Presentation, domain logic and data access are a familiar separation, with an application service boundary where operations need coordination.
- Layering narrows the responsibilities a developer must consider together.
- The chapter’s exact packages are a worked design decision, not a universal folder template.
The Program Already Has an Architecture
- Someone chooses its dependencies, owns its state and decides when effects occur. Those decisions exist even without a server or database.
- The question is whether they are explicit enough to review and change.
- The coordinator owns an
ArrayList: easy to follow, but it ties use-case orchestration to one storage mechanism.
The Cost of Extracting a Boundary
- Extracting a narrow storage contract demonstrates dependency direction and storage ownership without inventing persistence requirements.
- It creates another interface and requires a contract for ordering, snapshots and duplicate identifiers.
- If a program will remain a tiny script with one private list, it may not be worth it. Here it supports the structural learning goal and a concrete, independently testable boundary.
What Must Remain Unchanged
- Accepted and rejected reservation strings.
- Policy limits and half-open overlap rules.
- Directory failure distinctions, loan behaviour.
- Notification timing: storage still precedes notification; an expected notification failure still retains the reservation.
- The optional publisher keeps its declared partial-failure semantics.
What Does Change
- Callers import named packages.
- Direct
ReservationCoordinatorconstruction requires an additional repository argument. - Command-line launchers use fully qualified names.
- These must be documented even when every user-facing transcript stays identical.
- Behaviour is protected within the supplied sequential, in-memory version, not for an unseen application that reflects on class names or depends on the old constructor.
Packages and Their Types (1 of 2)
| Package | Responsibility | Representative types |
|---|---|---|
domain | Values, records and borrowing decisions | LoanPeriod, Reservation, BorrowingPolicy |
application | Use-case coordination and result formatting | ReservationCoordinator, DirectoryReservationService, LoanService |
application.port | Contracts needed by application operations | ReservationRepository, BorrowerDirectory, NotificationSender |
infrastructure.directory | Provider-specific translation | LegacyDirectoryAdapter, LegacyDirectoryClient |
Packages and Their Types (2 of 2)
| Package | Responsibility | Representative types |
|---|---|---|
infrastructure.notification | Selected local notification mechanisms | ConsoleNotificationSender, NotificationPublisher |
infrastructure.persistence | Concrete reservation storage | InMemoryReservationRepository |
presentation | Thin console interaction | ReservationConsole |
bootstrap | Assembly and executable demonstrations | ReservationApplication, Chapter10Demo |
- The persistence package names a storage responsibility. The implementation is in memory and loses data when the process ends.
Packages Communicate; Rules Enforce
- A package is a namespace. Public Java types can still be imported from anywhere.
- A folder named
domaindoes not stop a developer constructing a console sender inside it. - So the chapter states permitted production dependencies and adds a test that examines compiled class dependencies.
- Tests may cross boundaries to assemble examples; startup wiring has a deliberate exception because it must choose concrete implementations.
The Dependency Matrix
| Source package | Permitted local targets |
|---|---|
domain | domain |
application.port | domain, application.port |
application | domain, application.port, application |
infrastructure | domain, application (including ports), infrastructure |
presentation | domain, application, presentation |
bootstrap | every application package, and explicitly demonstrated isolated examples |
Avoid Mechanical Layer Multiplication
- A class does not need an interface merely because it crosses a package boundary.
DirectoryReservationServicestays a concrete application service. - Not every domain object needs a repository and a service.
LoanPeriodis a value-shaped object: storing it through its own repository would obscure its role as part of a reservation. - The loan service’s internal map stays where it was: this chapter extracts one justified boundary, not a generic storage framework.
Presentation Translates an Interaction
ReservationConsolereceives a directory-backed service and aPrintStream.- Its
reservemethod invokes the service and prints the existing result. - It neither selects a borrower category nor duplicates the seven-day student rule.
- A test supplies a byte buffer and checks exact output; a directory exception propagates without printing a success line.
Application Coordinates the Use Case
DirectoryReservationServiceperforms lookup and delegates a valid borrower to the coordinator.- The coordinator applies policy, checks conflicts, creates the record, stores it, invokes its sender.
LoanServicecoordinates checkout and return using the supplied clock.- These arrange several responsibilities to complete a use case. The label does not require a remote service, a DI framework or one service per entity.
Why ConfirmationFormatter Stays Here
- Existing returned strings are part of the observable contract, so it remains in
application. - It is also used by the console notification adapter.
- A future structured result could separate domain outcomes from wording, but doing it now would mix an API redesign with this restructuring.
Domain Expresses Meaning and Invariants
- Borrower categories, borrowing policies, reservations, loans and loan periods belong to the domain vocabulary.
- They do not know about console output, provider codes or repository implementations.
- Domain code may use standard-library types (
LocalDate, collections). Independence means avoiding orchestration and external mechanisms the rules do not need. - No full Domain-Driven Design set: first explain the actual identity, invariants and ownership.
Infrastructure Implements Selected Mechanisms
- The directory adapter translates the provider contract into
BorrowerDirectory; the in-memory repository implements storage; notification adapters write output or dispatch callbacks. - Their code depends on application-owned contracts and the domain types they handle.
- Infrastructure does not own eligibility: the seven-day limit in the directory adapter would tie policy to the provider representation.
- The repository should not send a notification when a record is added: orchestration decides when.
Where an Outside Collaborator Sits
- The equipment directory, the book’s data-backed collaborator, shows where any outside collaborator belongs (a data source, or something that scores or predicts).
- Contract
BorrowerDirectoryand its failureDirectoryException:application.port. Callers must tell an outage from an absent borrower. - Implementation
LegacyDirectoryAdapterwithLegacyDirectoryClient:infrastructure.directory. - Construction: bootstrap passes the adapter to
DirectoryReservationService.
The Same Three Positions for a Model
- A model-backed collaborator would occupy the same three positions: a narrow port with bounded outcomes and a defined failure; an adapter in infrastructure; construction in bootstrap.
- The application would not import the adapter.
- A test could substitute the port exactly as the directory tests do.
Reading Figure 10.1
- Each box is a group of packages; an arrow means classes in the first group may refer to classes in the second.
- The application has no arrow to infrastructure.
ReservationCoordinatorimportsReservationRepository, an application-owned contract;InMemoryReservationRepositoryimports and implements it; bootstrap constructs the implementation. - The figure shows source references only, not the order of calls.
Runtime Calls Travel Toward an Adapter
- At runtime the coordinator calls an object that happens to be an in-memory repository.
- That does not require its source to import the concrete class: interface dispatch connects the call to the implementation selected during assembly.
- Source dependency points toward the port; the runtime call goes through it.
Reading Figure 10.2
- Each box at the top is an object; time runs down the page; solid arrows are calls, dashed arrows returns.
- The coordinator may take more than one snapshot during a request. The diagram groups those reads: it does not claim one storage call or a transaction.
- The application still assumes one sequential session.
Check What the Code Actually References
DependencyRulesTestinvokes the JDK’sjdepson compiled production classes and rejects local dependency edges that contradict the policy.- It also checks that dependencies were actually discovered (no vacuous pass on missing output).
- A second test builds the package-to-package graph and searches for a cycle.
- It catches a fully qualified reference even with no import.
What the Dependency Test Cannot See
- Reflective class names in strings, runtime configuration relationships.
- A semantically misplaced rule that uses only permitted types.
- So source review and behaviour tests remain necessary.
- It needs a full Java 17 JDK with the
jdk.jdepsmodule. Package rules are project decisions, not universal rules the analyser infers.
Optional: Ports and Adapters
- The directory, sender and repository interfaces can be discussed as ports, with infrastructure implementations as adapters.
- The console drives the application; storage and notifications are collaborators the application calls.
- Consistent with Cockburn’s discussion of isolating application behaviour from runtime mechanisms.
- It does not require six adapters, a hexagonal deployment or a package per method. Use it only when it clarifies which side owns the contract.
Package Cohesion: What Changes Together
- A cohesive package holds types that change for the same reason and are used together.
- CCP, Common Closure Principle: things that change together belong together.
- CRP, Common Reuse Principle: things used together belong together, so a client is not forced to depend on classes it never uses.
- REP, Reuse/Release Equivalence Principle: the unit you reuse is the unit you release. This project has no separately released package, so REP is context, not a check.
- Test the cuts with concrete changes, not slogans: for each change, list the packages whose production code must change.
Five Change Requests (1 of 2)
| Change request | Packages that change | What it shows |
|---|---|---|
| Staff may reserve for 21 days | domain (StaffReservationRules) | The policy change stays in one package |
The provider renames its status code Y | infrastructure.directory | Provider vocabulary stays outside the application |
| The confirmation wording changes | application (ConfirmationFormatter) | Its users, including the console sender, see it without being edited |
Five Change Requests (2 of 2)
| Change request | Packages that change | What it shows |
|---|---|---|
| Reservations are stored in a file | infrastructure.persistence (a new class) and bootstrap | The coordinator is not edited: it names only the port |
| A third borrower category is added | domain, infrastructure.directory and bootstrap | One business idea touches three packages |
- Most changes touch one or two packages. The last row is a legitimate cost to record, not a flaw to hide. Always add a change you expect to be awkward.
Package Coupling
- Coupling between packages is the number of references that cross a package boundary.
- Habit 1: depend on a small contract where the other side may vary. The coordinator refers to
ReservationRepository, not the in-memory class. - Habit 2: keep the package graph free of cycles. If
applicationreferred toinfrastructure.notificationwhile it referred back, neither could be compiled, tested or replaced without the other (the Acyclic Dependencies Principle, ADP).
Layers Rule Out Some Cycles, Not All
- The layer matrix rules out cycles between layers, because each pair allows references in one direction only.
- It does not rule them out inside one layer: the matrix lets
infrastructure.directoryandinfrastructure.notificationrefer to each other, so a cycle between them would pass the direction test. - That is why
DependencyRulesTesthas the separate cycle check.
The Port Package and Reuse
application.portholds five small contracts and their failure types, so a class that needs one is not forced to depend on a large service.- If a port package grew to hold many unrelated contracts, splitting it would be worth considering: a client using only the sender should not be affected when the directory contract changes.
Package Stability
- A package is stable when it is costly to change: many depend on it, it depends on little.
- A package is unstable when little depends on it and it depends on many.
- Both kinds are needed. The Stable Dependencies Principle (SDP): dependencies should point towards stability, so code that changes often depends on code that changes rarely.
Counting Stability
- Afferent coupling Ca: classes outside P that depend on classes inside P (incoming).
- Efferent coupling Ce: classes inside P that depend on classes outside P (outgoing).
- Instability I = Ce / (Ca + Ce), from 0 (maximally stable) to 1 (maximally unstable).
- This book counts classes, treats a nested class as part of its outer class, counts only references between this project’s packages, and ignores the Java standard library.
Reading the Numbers
domainhas I = 0: sixteen classes depend on it and it depends on nothing else. Keep it small and free of mechanisms: a change can ripple everywhere.bootstraphas I = 1: nothing depends on it, so it can change freely, which suits the place where concrete choices are made.- Most arrows run from a higher I to a lower one:
application(0.31) toapplication.port(0.18) todomain(0.00).
The One Arrow That Does Not
infrastructure.notification(0.30) depends onapplication(0.31), becauseConsoleNotificationSenderuses the concreteConfirmationFormatter.- The difference is one hundredth, from a handful of classes: no defect.
- It is the arrow to examine first in a review: the one concrete dependency running from a mechanism to an application class.
An Isolated Package Has No Defined Instability
patternsandexamples.structurehave no references to or from the lending packages: Ca + Ce = 0, and the formula divides by zero. The tool prints undefined.- Do not report it as 0 (maximally stable) or as 1. It is simply isolated, as intended.
behaviouris referenced only by the Chapter 9 demo: Ca = 1, I = 0.
Numbers Are Questions, Not Grades
- With one to eight classes per package, adding a single class moves a ratio by several hundredths.
- A package can have a good number and a muddled responsibility; a poor number can be the right design (bootstrap).
- Compare numbers only from the same tool and counting rules.
Package Abstractness: The Stable Abstractions Principle
- SAP: a package that many others depend on cannot change freely, so it should be abstract enough to be extended without being edited.
- Martin measures abstractness A as the share of a package’s classes that are abstract classes or interfaces.
- This book does not compute A: it needs a second tool, and with one to eight classes per package the ratio moves with every class added. SAP is used as a reading question.
Reading SAP on the Version
application.port: contracts and their failure types, so mostly abstract, and stable (I = 0.18). SAP satisfied.bootstrap: concrete and unstable (I = 1.00). SAP satisfied: nothing depends on it.domaindeserves the question: sixteen classes depend on it, yet it is mostly concrete records and rules. It stays safe because it changes only with the borrowing vocabulary, andBorrowingPolicygives the varying part an abstract seam.- The warning sign would be a package that is stable, concrete and changes often.
Option B Is Rejected by Two Checks
- The direction check failed:
application.ReservationCoordinator -> infrastructure.notification.NotificationException: the coordinator must name the exception to catch it, so application would depend on infrastructure. - The cycle check also failed:
infrastructure.notificationalready depends onapplication. - The instability figures would not have shown it (application 0.31; notification 0.27).
- The rule: a type that callers must catch or inspect belongs with the contract they depend on, not with one implementation.
Identity Is a Domain Decision
- A reservation has an identifier such as R1.
- Whether two records with that identifier and different data mean an update or an invalid duplicate depends on the storage contract. This version is append-only and rejects duplicate identifiers.
Reservationis immutable and does not overrideequalsorhashCode: two objects with the same ID are not equal through inherited object equality.- The repository uses the explicit identifier as its key.
Entity and Value Object
- An entity is distinguished by its identity rather than its attributes. A value object is one where you care only about attributes and logic, treated as immutable (Evans).
- Calling a class an entity does not silently implement equality, persistence or a lifecycle.
- Making it immutable does not remove its domain identity.
Value Meaning vs. Java Value Equality
- A
LoanPeriodis characterised by its start and exclusive end, not by a generated identifier. - But the running class keeps inherited object equality: two periods with equal dates are distinct by
equals. The chapter preserves that rather than change equality during a package move. examples.structure.PeriodValueis a record demonstrating component-based equality and hashing: two equal-date records are one logical value in a set.- Adopting it in the real model is a separate, reviewed change.
Immutability Supports Snapshots
- An unmodifiable list prevents adding or removing through that list. It does not generally make the elements immutable.
- The repository’s snapshots are safe because
Reservationand its period are also immutable. - A previously returned snapshot stays unchanged when another reservation is stored.
- If a later reservation becomes mutable, a shallow copy no longer establishes a historical snapshot.
The Port
public interface ReservationRepository {
List<Reservation> findAll();
void add(Reservation reservation);
}- Insertion-ordered, stable, unmodifiable snapshots.
addrejects null or duplicate IDs without replacing existing records.- The in-memory adapter uses a
LinkedHashMapkeyed by reservation ID and returnsList.copyOfsnapshots.
Where the Port Lives
- The interface is in
application.portbecause the application use case consumes it. - Domain-owned repository contracts are another possible design when the domain’s needs justify that ownership.
- Location should follow actual responsibilities, not a rule that all repository interfaces belong in one layer.
- A deliberately smaller example than the classic repository: no data mapper, no query language.
What the Extraction Does Not Change
- The coordinator still calculates counts, checks conflicts and allocates size-based identifiers. None of that moves into storage.
- The factory supplies a fresh repository per coordinator: the earlier session ownership and R1 starting point stay.
- The new constructor is an injection point, not a promise to support preloaded or shared stores.
- No
delete, genericsave, transaction manager or repository for every class.
A Port Does Not Supply Transactions
- A test supplies a repository whose
addthrows before appending: the exception propagates and notification is not reached. - That establishes the order for this specific failure, not that a future database cannot fail after a write succeeded.
- Snapshots and duplicate-ID checks do not make count-check, conflict-check and append atomic across concurrent requests.
- A database would require reviewing those assumptions.
The Worked Decision
- ADR-010: named packages, application-owned ports, one in-memory reservation adapter, explicit bootstrap wiring.
- Status: implemented teaching example; peer review pending. The wording separates author verification from a review that has not occurred.
- Problem: construction and storage ownership were insufficiently visible as the example grew.
- Compares: keeping the unnamed package; narrow boundaries; a larger framework or generic repository.
ADR-010: Alternatives and Consequences
- Alternatives: keep the unnamed package and private list (less code, weaker visibility); narrow packages and one storage port (selected); a generic CRUD repository, ORM, database or DI framework (declined: not required).
- Consequences: new imports; fully qualified launcher names; one more coordinator constructor argument; a full-JDK dependency for the architecture test; more files to maintain.
- Revisit triggers: persistent storage; multiple writers; a cancellation lifecycle; a new identifier scheme; value equality for the period; structured results.
Your Decision Will Differ
- ADR-010 is the worked model for the book’s project. On your team’s project: a different boundary, different alternatives, different costs.
- Do not copy ADR-010’s decision unless it is also yours.
- An alternative of “do nothing” and a revisit trigger naming an event you could observe make a record more useful than one that only justifies what was already done.
A Reviewable Migration
- Separate package moves from the repository extraction and from newly added checks.
- Older tests move into a
regressionpackage; direct constructor calls receive the new repository; expected behaviours are retained. - The version is not evidence of Git commits or an opened pull request. Record your actual sequence and review comments.
Review One Scenario Across Boundaries
- Trace a valid staff request from presentation through directory lookup, policy evaluation, storage and notification.
- Then a missing borrower and an expected notification failure.
- Check the layer diagram matches both source dependencies and the observed order of effects.
- Ask: who creates each concrete adapter? Does application code import it? Does domain depend on application services?
Two Kinds of Evidence
- The dependency test examines compiled relationships. The regression suite examines selected behaviour.
- A semantically poor design can pass both. A reasoned human review remains part of the evidence.
- Verify that presentation has not copied a policy rule to make the demo pass.
- Inspect the repository’s snapshot and duplicate-ID contract.
Preparing Evidence (1 of 3)
| Item | What to produce | What it should establish |
|---|---|---|
| 1 | Pattern note | A pattern used where it removes a real problem, with the reason; one place you declined |
| 2 | Compact package diagram | Actual responsibilities and declared dependency direction, matching your code |
| 3 | Restructuring diff or PR | What moved and what logic changed, kept apart |
Preparing Evidence (2 of 3)
| Item | What to produce | What it should establish |
|---|---|---|
| 4 | Regression result and dependency report | Existing examples still pass; references follow the chosen rules; any cycle is reported |
| 5 | ADR | Problem, alternatives (including doing nothing), decision, consequences, revisit triggers |
| 6 | Actual review record | A reviewer examined the change and the feedback was addressed |
Preparing Evidence (3 of 3)
| Item | What to produce | What it should establish |
|---|---|---|
| 7 | Demonstration and retrospective | What worked, what did not, one next action |
| 8 | Limits statement | What the structure does not provide: persistence, concurrency safety, a cancellation lifecycle |
- Local author testing is not hosted CI evidence. A sample PR description is not a completed peer review.
The Activity
- Objective: examine a proposed dependency review without treating its wording as evidence.
- Supply the package map, the relevant Java files and the declared dependency policy.
- Limit the task to identifying references and responsibility concerns: do not ask the assistant to invent a database or rewrite requirements.
The Ready-Made Example
The coordinator calls an in-memory repository at runtime, so it must import infrastructure and violates dependency inversion. Move the repository interface into infrastructure. Every entity needs a repository and a service. Making the reservation list unmodifiable makes every future element immutable. The bootstrap imports adapters, so remove it and let services construct their dependencies. Since all tests pass, the pull request has been peer reviewed.
Findings (1 of 2)
| Fictional claim | Finding |
|---|---|
| A runtime call to the in-memory store proves an application import of infrastructure | Incorrect: interface dispatch and source references are different relationships |
| Put the repository interface in infrastructure | Not justified: the consuming application would depend on an infrastructure-owned contract |
| Every entity needs a repository and service | No such requirement follows from identity; LoanPeriod has neither |
Findings (2 of 2)
| Fictional claim | Finding |
|---|---|
| An unmodifiable list makes every element immutable | Incorrect for mutable elements |
| Bootstrap must not import adapters | Incorrect for the selected composition role |
| Passing tests establishes peer review | Incorrect evidence substitution: record real reviewer comments |
- A dependency review should name the referencing type and the referenced type. “Infrastructure coupling” without that trace is only a claim to investigate.
Mistakes 1 to 4
| Mistake | Consequence | Better action |
|---|---|---|
| Treat package names as enforced architecture | Public types can still form forbidden dependencies | State and check a dependency policy |
| Confuse runtime calls with source imports | Misdiagnoses interface-based dispatch | Trace both relationships separately |
| Move provider codes into the domain | Couples rules to an external representation | Keep translation in the adapter |
| Duplicate eligibility in presentation | Inconsistent entry points | Delegate the use case |
Mistakes 5 to 8
| Mistake | Consequence | Better action |
|---|---|---|
| Call immutable objects automatically value-equal | Misreads equals and collections | Inspect and test equality semantics |
| Add generic CRUD to every class | Invents unsupported lifecycle operations | Add only needed storage capabilities |
| Construct adapters inside application services | Hides concrete dependencies | Assemble them explicitly at bootstrap |
| Claim passing tests prove peer review | Substitutes one type of evidence for another | Record actual review separately |
Mistakes 9 to 11
| Mistake | Consequence | Better action |
|---|---|---|
| Grade a package by its instability number | A coarse number from few classes; a muddled package can score well | Use it to pick what to examine, then reason about responsibility |
| Report an isolated package as I = 0 or I = 1 | The formula is undefined when Ca + Ce = 0 | Report it as undefined and explain why |
| Accept a cycle because the layers look right | Two packages in one layer can still depend on each other | Check the package graph for cycles |
What You Start With, and the Demonstration
- You start with: the migrated version, the package responsibility table, the metrics table and your team’s repository.
- Demonstration: trace the coordinator’s storage access, then the port and adapter after extraction. Compare the source diagram with the runtime sequence. Run
tools/PackageMetrics.javaandDependencyRulesTest, and one demo under its new fully qualified launcher name. - Guided practice: classify a domain policy, directory adapter, application service and bootstrap class. Explain why the same concrete import is right in bootstrap and forbidden in application code.
B1: Classify
For StaffReservationRules, LegacyDirectoryAdapter, DirectoryReservationService, InMemoryReservationRepository and ReservationApplication: name the package, what it may refer to, and one thing it must not.
| Class | Package | Must not refer to |
|---|---|---|
StaffReservationRules | domain | Any application, infrastructure or presentation class |
LegacyDirectoryAdapter | infrastructure.directory | presentation, bootstrap, an isolated example |
DirectoryReservationService | application | Any infrastructure class |
InMemoryReservationRepository | infrastructure.persistence | presentation, bootstrap |
ReservationApplication | bootstrap | Nothing forbidden, but no business rule |
B2: Check the References
- Run the build and the compiled dependency check.
- A permitted reference:
LegacyDirectoryAdapter -> application.port.BorrowerDirectory. Infrastructure may refer to the application’s ports, and the adapter implements the contract. - The reference from
infrastructure.notificationtoapplication:ConsoleNotificationSender -> application.ConfirmationFormatter. The console sender builds its notice text with the shared formatter, preserving existing wording.
B3: Diagnose the Package Design
- (a) Write two change requests of your own and list the packages each would change. (Model: “the console prints the borrower’s name”:
presentationonly, orapplicationtoo, which exposes the string-result limit. “Add an email notifier”: a new class ininfrastructure.notificationand one construction change inbootstrap.) - (b) Identify the one arrow to a less stable package; would you act?
- (c) Should
ConfirmationFormatterstay inapplication? Name the test or number that would change your mind.
B3 (c): The Model Answer
- Stay in
application. The coordinator needs it to build the returned text: moving it intoinfrastructure.notificationwould make application depend on infrastructure, the same failure as movingNotificationException. - Moving it into
domainwould put wording in the domain. - What would change the answer: a second notification format needing its own formatter, or the dependency report showing more concrete application classes referenced from infrastructure.
T1 and T2
- T1. Diagram. A compact package diagram of your team’s codebase in the style of Figure 10.1: boxes for groups of packages, arrows for permitted references. Mark one reference you would forbid and say why. No packages yet? Propose the groups.
- T2. Decision. One architecture decision your team actually faces (where a boundary or port belongs; whether to extract a storage contract; where construction happens). An ADR in ADR-010’s form: context, decision, alternatives including doing nothing, consequences, revisit triggers.
T3, T4 and T5
- T3. Restructure. One small change on a branch. Package moves separate from logic changes; existing checks passing; say which behaviour you protected.
- T4. Check the direction. A compiled dependency check (
DependencyRulesTestorjdeps) if your build can; otherwise a manual import review, recorded as manual. Look for cycles as well as forbidden directions. - T5. Review. A teammate reviews the change and the ADR. Record the feedback and what you changed. Then work through the Milestone 3 checklist: which items are complete, which are gaps, who will close each.
What a Good Team Answer Shows
- T1: every arrow justified in one sentence; at least one forbidden reference marked; the dependency check confirms it does not occur. A weak diagram copies Figure 10.1 without checking.
- T2: the context names a problem someone could observe (“the checkout class creates the payment client and cannot be tested”); alternatives include doing nothing; the trigger is an event.
- T4: says whether the check was compiled or manual, lists forbidden references found or states none, says whether cycles were searched.
- T5: records a real reviewer’s comments and the response.
If You Want More: The Book’s Project
- B4. Break the rules on purpose. In a scratch copy, construct
InMemoryReservationRepositoryinsideReservationCoordinator. Run the dependency test, record the violation, restore the original. - B5. Reproduce the migration from the Chapter 9 project: move classes into the declared packages, extract the repository contract, supply it through construction, update imports and launchers, preserve results.
- B6. The Section 10.10 AI activity.
What You Should End Up With, and When You Are Done
- What you should end up with: answers to B1 to B3; for your team’s project the T1 diagram, T2 decision record, T3 diff or PR description, T4 dependency result, T5 review record with the checklist status.
- Accept when: your diagram matches your code; your decision states a real alternative and an observable revisit trigger; moves are apart from logic changes; dependency evidence covers direction and cycles and says compiled or manual; the review is real.
- Do not claim review or hosted CI that has not occurred.
Fixing the Flaws: Four Wrong Alternatives (1 of 2)
// WRONG: the application now names a concrete infrastructure type
private final ReservationRepository repository = new InMemoryReservationRepository();
// WRONG: callers could change the store without add's checks
public List<Reservation> findAll() { return backingList; }- The first: the dependency check rejects the compiled edge. The second: the snapshot test checks non-mutation and historical independence.
Fixing the Flaws: Four Wrong Alternatives (2 of 2)
// WRONG: a second object with the same ID replaces the first
records.put(reservation.id(), reservation);
// WRONG: the console now owns a policy the domain already decides
if (days > 7) { output.println("Rejected: borrower limit or duration."); return; }- The third: duplicate IDs must reject without mutation. The fourth: a ten-day staff request exposes it (staff policy allows it): the final console test sends exactly that.
Do the Tests Detect Faults? (1 of 3)
| Fault | Before the three added tests | With the final tests |
|---|---|---|
Duplicate id replaces the original; modifiable snapshot copy; live snapshot view; HashMap instead of insertion order | All four caught | Unchanged |
| Explicit null check removed in the repository | Survived: still throws NullPointerException, only the message differs | Survives (equivalent behaviour) |
Stores after notification; reservations() modifiable copy; identifier skips a number; conflict check ignores stored records | All four caught | Unchanged |
Do the Tests Detect Faults? (2 of 3)
| Fault | Before the three added tests | With the final tests |
|---|---|---|
| Null repository accepted | Survived: no test constructed a coordinator without one | Caught by coordinatorRequiresARepository |
| One repository shared by every session | Caught | Unchanged |
| Console prints without a newline; swallows directory failures | Both caught | Unchanged |
| Console duplicates the seven-day rule | Survived: the console test used only a student | Caught by the ten-day staff test |
Do the Tests Detect Faults? (3 of 3)
| Fault | Before the three added tests | With the final tests |
|---|---|---|
PeriodValue validation removed; LoanPeriod gains value equality | Both caught | Unchanged |
| Seven dependency-direction violations | All caught by the direction test | Unchanged |
directory and notification packages refer to each other | Survived: each reference is individually permitted | Caught by the cycle test |
| A class name held in a string | Survived: jdeps reads compiled references | Survives: a documented limit |
- Of the 25 faults, 20 caught before the three tests were added and 23 by the final tests.
The Test Inventory
| Test class | Invocations | Purpose |
|---|---|---|
RepositoryBoundaryTest | 7 | Snapshots, duplicate/null rejection, storage before notification, failed add, independent sessions, required repository |
ValueSemanticsTest | 2 | Record equality; preserved running period equality |
ReservationConsoleTest | 3 | Output contract, a staff request above the student limit, unhidden directory failure |
DependencyRulesTest | 2 | Compiled references against the matrix; no package cycle |
| Migrated earlier suite | 107 | Existing policies, directory, loan, notification, pattern, lifecycle |
| Total | 121 | Complete version gate |
Concept Questions
- Why can the coordinator call an infrastructure object without depending on its concrete class?
- What distinguishes an application service from a domain policy and a repository?
- Why does an immutable reservation still have entity identity, and why does that not automatically define Java equality?
- What can the
jdepscheck establish, and what does it leave for review?
Diagnosis Questions (1 of 2)
- A console class checks the seven-day limit before calling the service. A new API endpoint does not. Explain the responsibility problem and a test that exposes inconsistent behaviour.
- A developer adds a database repository and announces concurrent safety because
addrejects duplicate IDs. Identify the missing consistency question across eligibility, conflicts and insertion.
Diagnosis Questions (2 of 2)
- In the metrics table
bootstraphas I = 1.00 anddomainhas I = 0.00. Explain why neither is a defect and what a change to each would disturb. - A new class in
infrastructure.directoryrefers toinfrastructure.notification, and a class there already refers back to the directory package. Which check catches this, and which check does not?
Guidance: Questions 1 to 4
- The coordinator refers to
ReservationRepository; bootstrap supplies an implementation; at runtime dispatch reaches the concrete method. Static and runtime diagrams answer different questions. - A service coordinates a use case; a policy expresses a rule; a repository provides access to stored records through a stated contract.
- R1 identifies the reservation independent of its data; immutable fields do not remove that. Java equality is a separate implementation choice; this class keeps object equality.
jdepsreports compiled class relationships; the tests compare edges with the matrix and search for cycles. They do not prove responsibilities are well chosen, see reflection or strings, or establish transactionality.
Guidance: Questions 5 to 8
- The console and API disagree if only one copies the rule. Put it in the domain policy; delegate both to the same operation. Send the same student request through both; also test an allowed staff duration.
- Two requests can both observe a free interval and append different IDs. Unique IDs do not make eligibility, conflict check and insertion atomic.
bootstrapI = 1 can change freely, but must not accumulate business rules.domainI = 0: a change ripples into every package, so keep it small.- The cycle test catches it. The direction test does not: each reference is individually permitted.
Transfer Challenge: A Clinic
A clinic appointment application has eligibility rules, an external patient directory, a console view and an in-memory appointment list.
- Propose responsibilities and dependency directions without inventing deployment services.
- Choose one storage port, say which layer owns its contract, and distinguish an appointment’s identity from its scheduled-time value.
- Name one behaviour to protect during restructuring.
- Name one change request you would use to test whether your package cuts are cohesive.
Transfer Guidance
- Domain: appointment times, patient identity, eligibility. Application: the booking operation, with a patient-directory port and a focused appointment-store port. Infrastructure: the provider adapter and in-memory store. Presentation delegates; bootstrap selects.
- An appointment has an explicit identity; its interval is a value. Do not assume either gives Java
equals: specify interval validation, equality and duplicate-ID behaviour. - Preserve: a conflicting interval is rejected without storing another appointment or notifying anyone. Also preserve provider failure versus missing patient.
Summary
- Application structure makes responsibilities and dependency decisions visible beyond individual classes.
- The version adds named packages, a narrow storage port, explicit assembly and a thin presentation adapter while preserving tested outcomes.
- Package design adds four questions: what changes together and is released together, what clients depend on, whether dependencies point towards stability, and whether a stable package is abstract enough to extend.
Sources and Further Reading
- Fowler: Presentation Domain Data Layering; Stafford: Service Layer; Hieatt and Mee: Repository.
- Oracle: the Java 17
jdepscommand; Java SE 17List. - Cockburn: Hexagonal Architecture.
- Evans: Domain-Driven Design Reference (2015), pp. 10 to 17.
- Martin: Agile Software Development (2002): principles of package design.
- Full references and links are in the chapter text.