Chapter 10 Companion: Code, Explanations and Solutions
Code explanations and solutions for Chapter 10 Companion: Code, Explanations and Solutions.
From Classes to Application Structure OO Development and Agile Practices
1. Scope and execution
This file accompanies Chapter 10, From Classes to Application Structure. It contains the complete restructured version, migration instructions, an architecture decision, tests and model solutions. The starting implementation is the Chapter 9 companion.
The change introduces named application packages, a focused reservation repository boundary and a thin console adapter. It preserves the demonstrated use-case outcomes. Package names, imports, constructor wiring and launcher names change; source and binary compatibility with the unnamed-package version are not claimed.
1.1 Reconstruct a clean version
Create a new directory named after. Save every labelled file listing in Sections 3–5 at its indicated path, including .gitattributes and tools/PackageMetrics.java. Copy only fence contents, preserving case and creating intermediate directories. README and ADR listings use four backticks where appropriate. Start with a clean directory: leaving the old unnamed-package source files beside the migrated files would create a misleading mixed version.
Use a full Java 17 JDK, including the jdk.jdeps module, and Maven 3.9.11. JUnit Jupiter 5.13.4, Mockito 5.20.0, Compiler Plugin 3.14.1 and Surefire 3.5.4 remain pinned. The Maven project version becomes 0.10.0. Initial builds require dependency access.
This command reports dependencies; DependencyRulesTest applies this project’s selected rules to them. A full JDK is required even if a smaller runtime could launch the application.
1.2 Expected output and earlier launchers
The new demonstration prints the same five lines as Chapter 8’s example, now through a thin console adapter:
Confirmed: R1 for B2 on CAM1 [2026-10-20, 2026-10-30).
Rejected: borrower limit or duration.
Rejected: borrower not found or inactive.
Reservations: 1
Notices: 1
Earlier demonstrations now use fully qualified names:
Chapter04Demo, Chapter05Demo and ReservationApplication are likewise in edu.se506.lending.bootstrap. The old command java -cp target/classes Chapter09Demo no longer names the migrated class. This is a documented launcher change, not a failed preservation of the demo’s business output.
1.3 Package metrics
The metrics in Section 10.5 of the chapter come from a small program that is not part of the Maven build. After compiling, run it from the project root:
The counting rules are in the program’s header comment and the chapter: classes are the unit, a nested class counts with its outer class, only this project’s own packages count, and the standard library is ignored. Ca is the number of classes outside a package that depend on it, Ce the number of its classes that depend on classes outside it, and I = Ce / (Ca + Ce). When Ca + Ce is zero the program prints undefined instead of guessing. Other tools count differently, so compare numbers only from one tool.
2. Migration and design map
2.1 Package mapping
All package names in this table are beneath edu.se506.lending unless otherwise stated.
Existing top-level patterns and behaviour packages
Earlier isolated examples and their tests, unchanged
New top-level examples.structure
PeriodValue equality demonstration, not part of the running domain
Expected-failure types belong to the application ports because callers should not need provider-specific exception types. The adapter still translates the provider’s declared failure to the application-facing exception.
2.2 Constructor and storage change
Previously, ReservationCoordinator owned an ArrayList directly and accepted policies, formatter and sender. It now accepts those three dependencies plus a ReservationRepository as its fourth argument. Its scans use repository snapshots, insertion uses add, and reservations() returns the repository’s specified snapshot.
ReservationApplication.create(sender) constructs a fresh InMemoryReservationRepository and passes it to the coordinator. Direct coordinator constructions in the earlier checks and Strategy example are updated with the same explicit adapter. Their assertions and expected outcomes remain intact.
No compatibility constructor instantiates infrastructure inside the application package. Such a shortcut would undermine the dependency boundary being demonstrated. Consumers update construction or use the factory in its new bootstrap package.
The repository contract is append-only, insertion ordered, and keyed by explicit reservation ID. Duplicate IDs reject without replacement. Null addition rejects. Snapshot values are safe here because the Reservation graph is immutable. The port does not impose a generic CRUD lifecycle or promise database transaction semantics.
2.3 Supported ownership and preserved semantics
The factory supplies one new, empty repository for one sequential coordinator/session. Size-based R1, R2 allocation is retained. Arbitrary preloaded stores, deletion, multiple writers or shared independent loan services would need additional contracts and are not promised by introducing constructor injection.
Storage still precedes notification. A repository that throws before adding does not trigger notification. A future implementation that writes and then loses acknowledgement presents a different failure case; this chapter does not claim to solve it.
The existing running LoanPeriod remains immutable but keeps inherited Java object equality. PeriodValue separately demonstrates record equality and hashing. No entity lifecycle, policy limit, availability rule or cancellation behaviour changes during this restructuring.
2.4 Declared production dependency rules
Source package
Permitted local targets
Domain
Domain
Application ports
Domain and application ports
Application services
Domain and application, including ports
Infrastructure
Domain, application and infrastructure
Presentation
Domain, application and presentation
Bootstrap
All application packages and explicitly demonstrated isolated examples
Ordinary production layers cannot depend on patterns, behaviour or examples. Bootstrap may launch an isolated demonstration; test setups may assemble whatever they need. The gate examines compiled production classes, not test classes. Standard-library dependencies are outside this local package matrix.
The test catches local bytecode dependency edges, including fully qualified references. A second test builds the package-to-package graph from the same edges and fails if it contains a cycle; the matrix alone would let two infrastructure packages refer to each other. The cycle check looks at the lending packages only. It is not an enforcement of all possible third-party-library policies, reflection, configuration or semantic responsibility. The permitted infrastructure-to-application edge accommodates ports and the retained shared ConfirmationFormatter.
2.5 Assertions and fixes carried forward from Chapters 3–9
Several strengthened checks do not change the JUnit count, so a green total cannot show that a migration kept them. This table lists what must survive, and where it lives in this version.
Origin
Preserved item
Location in this version
Chapter 3
The AC-03 request that ends exactly where an existing reservation starts, as well as the request that starts where one ends (five acceptance groups)
regression/Chapter03AcceptanceExamples.java
Chapter 4
.gitattributes keeping scripts/check.sh in LF form
.gitattributes
Chapter 6
BrokenSender: an unexpected sender failure is not swallowed and the record is stored first (28 checks)
regression/Chapter06Checks.java
Chapters 6–8
serialVersionUID on NotificationException, DirectoryException, LegacyDirectoryClient.Failure and PatternExamples.DeliveryFailure
those four files
Chapter 7
assertThrows(UnsupportedOperationException.class, snapshot::clear) on the loan snapshot
Command construction rejects a null receiver, a blank borrower, a null item and a null start, and the confirmation is compared exactly, period included
regression/ReserveEquipmentCommandTest.java
To check a migration yourself, compare each migrated file with its Chapter 9 original, ignoring package and import lines. In this version the only differences are the repository argument added to coordinator constructions (in BehaviourStrategyTest, Chapter06Checks and ReservationApplication) and the coordinator’s own storage code.
2.6 What the package metrics do and do not show
The table in Section 1.3 is a measurement of the compiled version, not a judgement. With one to eight classes per package a single added class moves a ratio by several hundredths. The one arrow that points to a slightly less stable package (infrastructure.notification at 0.30 to application at 0.31) is the concrete ConfirmationFormatter dependency that Section 6.2 already describes. The isolated packages patterns and examples.structure have no local references in either direction, so their instability is undefined; behaviour is referenced by Chapter09Demo, so Ca = 1 and I = 0.
3. Complete production and isolated example sources
The following files reconstruct the complete version. Package declarations and directory paths are part of each listing and must be preserved.
packagebehaviour;/** Proposed lifecycle only: not integrated with ReservationCoordinator or LoanService. */publicfinalclassLifecycleExamples{privateLifecycleExamples(){}publicenumStatus{REQUESTED,CONFIRMED,ON_LOAN,RETURNED,CANCELLED}publicinterfaceLifecycle{Statusstatus();voidconfirm();voidcheckOut();voidreturnItem();voidcancel();}// Starting design: operation-specific switches repeat state decisions.publicstaticfinalclassConditionalReservationimplementsLifecycle{privateStatusstatus=Status.REQUESTED;publicStatusstatus(){returnstatus;}publicvoidconfirm(){switch(status){caseREQUESTED->status=Status.CONFIRMED;default->throwrejected("confirm",status);}}publicvoidcheckOut(){switch(status){caseCONFIRMED->status=Status.ON_LOAN;default->throwrejected("check out",status);}}publicvoidreturnItem(){switch(status){caseON_LOAN->status=Status.RETURNED;default->throwrejected("return",status);}}publicvoidcancel(){switch(status){caseREQUESTED,CONFIRMED->status=Status.CANCELLED;default->throwrejected("cancel",status);}}}// Completed State design: context delegates to the current state object.publicstaticfinalclassStateReservationimplementsLifecycle{privatePhasephase=Phase.REQUESTED;publicStatusstatus(){returnphase.status;}publicvoidconfirm(){phase=phase.confirm();}publicvoidcheckOut(){phase=phase.checkOut();}publicvoidreturnItem(){phase=phase.returnItem();}publicvoidcancel(){phase=phase.cancel();}}privateenumPhase{REQUESTED(Status.REQUESTED){@OverridePhaseconfirm(){returnCONFIRMED;}@OverridePhasecancel(){returnCANCELLED;}},CONFIRMED(Status.CONFIRMED){@OverridePhasecheckOut(){returnON_LOAN;}@OverridePhasecancel(){returnCANCELLED;}},ON_LOAN(Status.ON_LOAN){@OverridePhasereturnItem(){returnRETURNED;}},RETURNED(Status.RETURNED),CANCELLED(Status.CANCELLED);finalStatusstatus;Phase(Statusstatus){this.status=status;}Phaseconfirm(){throwrejected("confirm",status);}PhasecheckOut(){throwrejected("check out",status);}PhasereturnItem(){throwrejected("return",status);}Phasecancel(){throwrejected("cancel",status);}}privatestaticIllegalStateExceptionrejected(Stringaction,Statusstatus){returnnewIllegalStateException("Cannot "+action+" in "+status);}}
packageedu.se506.lending.application;/** Executes synchronously on the caller's thread; no queue or retry is implied. */@FunctionalInterfacepublicinterfaceCommand<R>{Rexecute();}
packageedu.se506.lending.application;importedu.se506.lending.domain.Reservation;importjava.util.Objects;publicfinalclassConfirmationFormatter{publicStringformat(Reservationreservation){Objects.requireNonNull(reservation,"reservation");return"Confirmed: "+reservation.id()+" for "+reservation.borrowerId()+" on "+reservation.itemId()+" ["+reservation.period().start()+", "+reservation.period().endExclusive()+").";}}
packageedu.se506.lending.application;importedu.se506.lending.application.port.BorrowerDirectory;importedu.se506.lending.application.port.DirectoryException;importedu.se506.lending.domain.Borrower;importjava.time.LocalDate;importjava.util.Objects;/** Directory-backed entry point; this does not authenticate the caller. */publicfinalclassDirectoryReservationService{privatefinalBorrowerDirectorydirectory;privatefinalReservationCoordinatorcoordinator;publicDirectoryReservationService(BorrowerDirectorydirectory,ReservationCoordinatorcoordinator){this.directory=Objects.requireNonNull(directory,"directory");this.coordinator=Objects.requireNonNull(coordinator,"coordinator");}publicStringreserve(StringborrowerId,StringitemId,LocalDatestart,intdays){if(borrowerId==null||borrowerId.isBlank()||itemId==null||itemId.isBlank()){thrownewIllegalArgumentException("Identifiers must not be blank.");}Objects.requireNonNull(start,"start");varfound=directory.find(borrowerId);if(found.isEmpty())return"Rejected: borrower not found or inactive.";Borrowerborrower=found.get();if(!borrowerId.equals(borrower.id())){thrownewDirectoryException("Directory returned a different borrower");}returncoordinator.reserve(borrower.id(),itemId,start,days,borrower.category());}}
packageedu.se506.lending.application;importedu.se506.lending.domain.Loan;importedu.se506.lending.domain.Reservation;importjava.time.Clock;importjava.time.LocalDate;importjava.util.LinkedHashMap;importjava.util.List;importjava.util.Map;importjava.util.Objects;/** One service per coordinator/session; sequential, in-memory teaching flow. */publicfinalclassLoanService{privatefinalReservationCoordinatorreservations;privatefinalClockclock;privatefinalMap<String,Loan>loans=newLinkedHashMap<>();publicLoanService(ReservationCoordinatorreservations,Clockclock){this.reservations=Objects.requireNonNull(reservations,"reservations");this.clock=Objects.requireNonNull(clock,"clock");}publicLoancheckOut(StringreservationId){Reservationreservation=reservations.reservations().stream().filter(r->r.id().equals(reservationId)).findFirst().orElseThrow(()->newIllegalArgumentException("Unknown reservation."));if(loans.containsKey(reservationId)){thrownewIllegalStateException("Reservation already used for a loan.");}LocalDatetoday=LocalDate.now(clock);if(today.isBefore(reservation.period().start())||!today.isBefore(reservation.period().endExclusive())){thrownewIllegalStateException("Checkout must be within the reservation period.");}booleanitemAlreadyOut=loans.values().stream().anyMatch(loan->loan.active()&&loan.itemId().equals(reservation.itemId()));if(itemAlreadyOut)thrownewIllegalStateException("Item is already on loan.");Loanloan=newLoan(reservation.id(),reservation.borrowerId(),reservation.itemId(),today,null);loans.put(reservationId,loan);returnloan;}publicLoanreturnLoan(StringreservationId){Loancurrent=loans.get(reservationId);if(current==null||!current.active()){thrownewIllegalStateException("No active loan for this reservation.");}Loanreturned=newLoan(current.reservationId(),current.borrowerId(),current.itemId(),current.borrowedOn(),LocalDate.now(clock));loans.put(reservationId,returned);returnreturned;}publicList<Loan>loans(){returnList.copyOf(loans.values());}}
packageedu.se506.lending.application;importedu.se506.lending.application.port.NotificationException;importedu.se506.lending.application.port.NotificationSender;importedu.se506.lending.application.port.ReservationRepository;importedu.se506.lending.domain.BorrowerCategory;importedu.se506.lending.domain.BorrowingPolicy;importedu.se506.lending.domain.LoanPeriod;importedu.se506.lending.domain.Reservation;importjava.time.LocalDate;importjava.util.List;importjava.util.Objects;importjava.util.Map;publicfinalclassReservationCoordinator{privatefinalReservationRepositoryrepository;privatefinalMap<BorrowerCategory,BorrowingPolicy>policies;privatefinalConfirmationFormatterformatter;privatefinalNotificationSendernotifications;publicReservationCoordinator(Map<BorrowerCategory,BorrowingPolicy>policies,ConfirmationFormatterformatter,NotificationSendernotifications,ReservationRepositoryrepository){this.policies=Map.copyOf(Objects.requireNonNull(policies,"policies"));for(BorrowerCategorycategory:BorrowerCategory.values()){if(!this.policies.containsKey(category)){thrownewIllegalArgumentException("Missing policy: "+category);}}this.formatter=Objects.requireNonNull(formatter,"formatter");this.notifications=Objects.requireNonNull(notifications,"notifications");this.repository=Objects.requireNonNull(repository,"repository");}publicStringreserve(StringborrowerId,StringitemId,LocalDatestart,intrequestedDays){returnreserve(borrowerId,itemId,start,requestedDays,BorrowerCategory.STUDENT);}publicStringreserve(StringborrowerId,StringitemId,LocalDatestart,intrequestedDays,BorrowerCategorycategory){Objects.requireNonNull(category,"category");if(borrowerId==null||borrowerId.isBlank()||itemId==null||itemId.isBlank()){thrownewIllegalArgumentException("Identifiers must not be blank.");}Objects.requireNonNull(start,"start");intactiveCount=0;for(Reservationexisting:repository.findAll()){if(existing.borrowerId().equals(borrowerId)){activeCount++;}}if(!policies.get(category).mayRequest(activeCount,requestedDays)){return"Rejected: borrower limit or duration.";}LoanPeriodrequested=newLoanPeriod(start,start.plusDays(requestedDays));if(hasConflict(itemId,requested)){return"Rejected: item unavailable.";}Reservationreservation=newReservation("R"+(repository.findAll().size()+1),borrowerId,itemId,requested);repository.add(reservation);Stringconfirmation=formatter.format(reservation);try{notifications.send(reservation);}catch(NotificationExceptionfailure){returnconfirmation+" Notification failed; reservation retained.";}returnconfirmation;}publicbooleanisAvailable(StringitemId,LocalDatestart,LocalDateendExclusive){if(itemId==null||itemId.isBlank()){thrownewIllegalArgumentException("Item identifier must not be blank.");}LoanPeriodrequested=newLoanPeriod(start,endExclusive);return!hasConflict(itemId,requested);}privatebooleanhasConflict(StringitemId,LoanPeriodrequested){for(Reservationexisting:repository.findAll()){if(existing.itemId().equals(itemId)&&existing.period().overlaps(requested)){returntrue;}}returnfalse;}publicList<Reservation>reservations(){returnrepository.findAll();}}
packageedu.se506.lending.application;importjava.time.LocalDate;importjava.util.Objects;/** Captures a request and receiver; constructing it performs no reservation. */publicrecordReserveEquipmentCommand(DirectoryReservationServicereceiver,StringborrowerId,StringitemId,LocalDatestart,intdays)implementsCommand<String>{publicReserveEquipmentCommand{Objects.requireNonNull(receiver,"receiver");if(borrowerId==null||borrowerId.isBlank()||itemId==null||itemId.isBlank()){thrownewIllegalArgumentException("Identifiers must not be blank.");}Objects.requireNonNull(start,"start");}@OverridepublicStringexecute(){returnreceiver.reserve(borrowerId,itemId,start,days);}}
packageedu.se506.lending.application.port;importedu.se506.lending.domain.Borrower;importjava.util.Optional;/** Empty means absent or inactive, never a provider outage. */@FunctionalInterfacepublicinterfaceBorrowerDirectory{Optional<Borrower>find(StringborrowerId);}
packageedu.se506.lending.application.port;importedu.se506.lending.domain.Reservation;/**
* Accept one non-null confirmed reservation for this implementation's local output.
* Do not modify the reservation or coordinator. Report expected output failure with
* NotificationException. Normal return is not a promise of email delivery.
*/@FunctionalInterfacepublicinterfaceNotificationSender{voidsend(Reservationreservation);}
packageedu.se506.lending.application.port;importedu.se506.lending.domain.Reservation;importjava.util.List;/** Minimal append-only store contract for one sequential session. */publicinterfaceReservationRepository{/** Stable unmodifiable snapshot, in insertion order. Values are immutable. */List<Reservation>findAll();/** Add a non-null reservation; duplicate IDs reject without changing the store. */voidadd(Reservationreservation);}
packageedu.se506.lending.bootstrap;importedu.se506.lending.application.LoanService;importedu.se506.lending.application.ReservationCoordinator;importedu.se506.lending.domain.Loan;importedu.se506.lending.infrastructure.notification.RecordingNotificationSender;importjava.time.Clock;importjava.time.Instant;importjava.time.LocalDate;importjava.time.ZoneOffset;publicfinalclassChapter07Demo{publicstaticvoidmain(String[]args){RecordingNotificationSenderinbox=newRecordingNotificationSender();ReservationCoordinatorreservations=ReservationApplication.create(inbox);LocalDateday=LocalDate.of(2026,10,20);System.out.println("Available before reservation: "+reservations.isAvailable("CAM1",day,day.plusDays(2)));System.out.println(reservations.reserve("B1","CAM1",day,2));System.out.println("Available after reservation: "+reservations.isAvailable("CAM1",day,day.plusDays(2)));LoanServiceloans=newLoanService(reservations,Clock.fixed(Instant.parse("2026-10-20T12:00:00Z"),ZoneOffset.UTC));System.out.println("Loan active after checkout: "+loans.checkOut("R1").active());System.out.println("Loan active after return: "+loans.returnLoan("R1").active());System.out.println("Accepted-reservation notices: "+inbox.received().size());}}
packageedu.se506.lending.domain;importjava.util.Objects;publicrecordBorrower(Stringid,BorrowerCategorycategory){publicBorrower{if(id==null||id.isBlank())thrownewIllegalArgumentException("Borrower id required");Objects.requireNonNull(category,"category");}}
packageedu.se506.lending.domain;/**
* A deterministic, side-effect-free borrowing decision for every pair of ints.
* Return false for negative counts or nonpositive durations; do not throw for them.
* Return false for requests outside this implementation's documented limits.
* A true result concerns count/duration only, not item availability or identity.
*/publicinterfaceBorrowingPolicy{booleanmayRequest(intactiveReservations,intrequestedDays);}
packageedu.se506.lending.domain;importjava.time.LocalDate;importjava.util.Objects;/** Immutable loan state. A null returnedOn means this loan is active. */publicrecordLoan(StringreservationId,StringborrowerId,StringitemId,LocalDateborrowedOn,LocalDatereturnedOn){publicLoan{if(reservationId==null||reservationId.isBlank()||borrowerId==null||borrowerId.isBlank()||itemId==null||itemId.isBlank()){thrownewIllegalArgumentException("Loan identifiers must not be blank.");}Objects.requireNonNull(borrowedOn,"borrowedOn");if(returnedOn!=null&&returnedOn.isBefore(borrowedOn)){thrownewIllegalArgumentException("Return cannot precede checkout.");}}publicbooleanactive(){returnreturnedOn==null;}}
packageedu.se506.lending.domain;importjava.time.LocalDate;importjava.util.Objects;publicfinalclassLoanPeriod{privatefinalLocalDatestart;privatefinalLocalDateendExclusive;publicLoanPeriod(LocalDatestart,LocalDateendExclusive){this.start=Objects.requireNonNull(start,"start");this.endExclusive=Objects.requireNonNull(endExclusive,"endExclusive");if(!start.isBefore(endExclusive)){thrownewIllegalArgumentException("End must be after start.");}}publicLocalDatestart(){returnstart;}publicLocalDateendExclusive(){returnendExclusive;}publicbooleanoverlaps(LoanPeriodother){Objects.requireNonNull(other,"other");returnstart.isBefore(other.endExclusive)&&other.start.isBefore(endExclusive);}}
packageedu.se506.lending.domain;importjava.util.Objects;publicfinalclassReservation{privatefinalStringid;privatefinalStringborrowerId;privatefinalStringitemId;privatefinalLoanPeriodperiod;publicReservation(Stringid,StringborrowerId,StringitemId,LoanPeriodperiod){this.id=requireText(id);this.borrowerId=requireText(borrowerId);this.itemId=requireText(itemId);this.period=Objects.requireNonNull(period,"period");}privatestaticStringrequireText(Stringvalue){if(value==null||value.isBlank()){thrownewIllegalArgumentException("Identifiers must not be blank.");}returnvalue;}publicStringid(){returnid;}publicStringborrowerId(){returnborrowerId;}publicStringitemId(){returnitemId;}publicLoanPeriodperiod(){returnperiod;}}
packageedu.se506.lending.domain;/** Illustrative teaching policy, not an approved institutional rule. */publicfinalclassStaffReservationRulesimplementsBorrowingPolicy{@OverridepublicbooleanmayRequest(intactiveReservations,intrequestedDays){returnactiveReservations>=0&&activeReservations<5&&requestedDays>=1&&requestedDays<=14;}}
packageedu.se506.lending.infrastructure.directory;/** Illustrative provider contract, not an actual university API or network client. */@FunctionalInterfacepublicinterfaceLegacyDirectoryClient{// null means no row; codes are exact and case-sensitive.Personlookup(Stringkey);recordPerson(Stringkey,Stringkind,Stringstatus){}finalclassFailureextendsRuntimeException{privatestaticfinallongserialVersionUID=1L;publicFailure(Stringmessage){super(message);}}}
packageedu.se506.lending.infrastructure.notification;importedu.se506.lending.application.port.NotificationException;importedu.se506.lending.application.port.NotificationSender;importedu.se506.lending.domain.Reservation;importjava.util.ArrayList;importjava.util.List;importjava.util.Objects;/** Sequential, in-process Observer example. No retries or durable delivery. */publicfinalclassNotificationPublisherimplementsNotificationSender{@FunctionalInterfacepublicinterfaceObserver{voidonConfirmed(Reservationreservation);}@FunctionalInterfacepublicinterfaceSubscriptionextendsAutoCloseable{@Overridevoidclose();}privatestaticfinalclassEntry{privatefinalObserverobserver;privateEntry(Observerobserver){this.observer=observer;}}privatefinalList<Entry>entries=newArrayList<>();privatebooleanpublishing;publicSubscriptionsubscribe(Observerobserver){Objects.requireNonNull(observer,"observer");if(entries.stream().anyMatch(entry->entry.observer==observer)){thrownewIllegalArgumentException("Observer already subscribed");}Entryentry=newEntry(observer);entries.add(entry);return()->entries.remove(entry);}@Overridepublicvoidsend(Reservationreservation){Objects.requireNonNull(reservation,"reservation");if(publishing)thrownewIllegalStateException("Reentrant publication is not supported");publishing=true;try{NotificationExceptionaggregate=null;for(Entryentry:List.copyOf(entries)){try{entry.observer.onConfirmed(reservation);}catch(NotificationExceptionfailure){if(aggregate==null)aggregate=newNotificationException("One or more observers failed");aggregate.addSuppressed(failure);}}if(aggregate!=null)throwaggregate;}finally{publishing=false;}}}
packageedu.se506.lending.infrastructure.persistence;importedu.se506.lending.application.port.ReservationRepository;importedu.se506.lending.domain.Reservation;importjava.util.LinkedHashMap;importjava.util.List;importjava.util.Map;importjava.util.Objects;/** Insertion ordered, append only, not persistent and not thread-safe. */publicfinalclassInMemoryReservationRepositoryimplementsReservationRepository{privatefinalMap<String,Reservation>records=newLinkedHashMap<>();@OverridepublicList<Reservation>findAll(){returnList.copyOf(records.values());}@Overridepublicvoidadd(Reservationreservation){Objects.requireNonNull(reservation,"reservation");if(records.containsKey(reservation.id())){thrownewIllegalArgumentException("Duplicate reservation id: "+reservation.id());}records.put(reservation.id(),reservation);}}
packageedu.se506.lending.presentation;importedu.se506.lending.application.DirectoryReservationService;importjava.io.PrintStream;importjava.time.LocalDate;importjava.util.Objects;/** Thin output adapter: delegates the use case and prints its existing result. */publicfinalclassReservationConsole{privatefinalDirectoryReservationServiceservice;privatefinalPrintStreamoutput;publicReservationConsole(DirectoryReservationServiceservice,PrintStreamoutput){this.service=Objects.requireNonNull(service,"service");this.output=Objects.requireNonNull(output,"output");}publicvoidreserve(StringborrowerId,StringitemId,LocalDatestart,intdays){output.println(service.reserve(borrowerId,itemId,start,days));}}
packageexamples.structure;importjava.time.LocalDate;importjava.util.Objects;/** Separate equality example; deliberately does not replace cumulative LoanPeriod. */publicrecordPeriodValue(LocalDatestart,LocalDateendExclusive){publicPeriodValue{Objects.requireNonNull(start,"start");Objects.requireNonNull(endExclusive,"endExclusive");if(!start.isBefore(endExclusive))thrownewIllegalArgumentException("End must be after start.");}}
packagepatterns;importjava.time.LocalDate;importjava.util.ArrayList;importjava.util.List;importjava.util.Objects;/** Isolated teaching examples; none is wired into the lending application. */publicfinalclassPatternExamples{privatePatternExamples(){}// Factory Method: inherited workflow invokes an overridable creation hook.publicinterfaceExporter{Stringexport(Stringvalue);}publicabstractstaticclassExportJob{protectedabstractExportercreateExporter();publicfinalStringrun(Stringvalue){Objects.requireNonNull(value,"value");returncreateExporter().export(value);}}publicstaticfinalclassPlainJobextendsExportJob{@OverrideprotectedExportercreateExporter(){returnvalue->"TEXT: "+value;}}publicstaticfinalclassHtmlJobextendsExportJob{@OverrideprotectedExportercreateExporter(){returnvalue->"<p>"+escape(value)+"</p>";}}// Simple factory: useful, but no overridden creation hook.publicstaticExportersimpleExporter(booleanhtml){returnhtml?value->"<p>"+escape(value)+"</p>":value->"TEXT: "+value;}// Abstract Factory: two product roles, supplied as a related family.publicinterfaceHeading{Stringrender(Stringtext);}publicinterfaceBody{Stringrender(Stringtext);}publicinterfaceReceiptFamily{Headingheading();Bodybody();}publicstaticfinalclassPlainFamilyimplementsReceiptFamily{publicHeadingheading(){returntext->text+"\n";}publicBodybody(){returntext->text+"\n";}}publicstaticfinalclassHtmlFamilyimplementsReceiptFamily{publicHeadingheading(){returntext->"<h1>"+escape(text)+"</h1>";}publicBodybody(){returntext->"<p>"+escape(text)+"</p>";}}publicstaticStringreceipt(ReceiptFamilyfamily,Stringtitle,Stringdetail){Objects.requireNonNull(family,"family");returnfamily.heading().render(title)+family.body().render(detail);}privatestaticStringescape(Stringtext){returnObjects.requireNonNull(text,"text").replace("&","&").replace("<","<").replace(">",">");}// Fluent builder variant: staged configuration, one immutable product.publicstaticfinalclassReportRequest{privatefinalStringborrowerId;privatefinalLocalDatefrom;privatefinalLocalDatetoExclusive;privatefinalbooleanincludeReturned;privatefinalList<String>columns;privateReportRequest(Builderbuilder){if((builder.from==null)!=(builder.toExclusive==null)){thrownewIllegalArgumentException("Both dates or neither are required");}if(builder.from!=null&&!builder.from.isBefore(builder.toExclusive)){thrownewIllegalArgumentException("Positive interval required");}borrowerId=builder.borrowerId;from=builder.from;toExclusive=builder.toExclusive;includeReturned=builder.includeReturned;columns=List.copyOf(builder.columns);}publicStringborrowerId(){returnborrowerId;}publicLocalDatefrom(){returnfrom;}publicLocalDatetoExclusive(){returntoExclusive;}publicbooleanincludeReturned(){returnincludeReturned;}publicList<String>columns(){returncolumns;}publicstaticBuilderforBorrower(Stringid){returnnewBuilder(id);}publicstaticfinalclassBuilder{privatefinalStringborrowerId;privateLocalDatefrom;privateLocalDatetoExclusive;privatebooleanincludeReturned;privatefinalList<String>columns=newArrayList<>();privateBuilder(Stringid){if(id==null||id.isBlank())thrownewIllegalArgumentException("Borrower id required");borrowerId=id;}publicBuilderduring(LocalDatefrom,LocalDatetoExclusive){Objects.requireNonNull(from,"from");Objects.requireNonNull(toExclusive,"toExclusive");this.from=from;this.toExclusive=toExclusive;returnthis;}publicBuilderincludeReturned(booleanvalue){includeReturned=value;returnthis;}publicBuildercolumn(Stringvalue){if(value==null||value.isBlank())thrownewIllegalArgumentException("Column required");columns.add(value);returnthis;}publicReportRequestbuild(){returnnewReportRequest(this);}}}// Decorators: same interface, explicit order and expected failure type.publicinterfaceSender{voidsend(Stringmessage);}publicstaticfinalclassDeliveryFailureextendsRuntimeException{privatestaticfinallongserialVersionUID=1L;publicDeliveryFailure(Stringmessage){super(message);}}publicstaticfinalclassAuditedSenderimplementsSender{privatefinalSenderdelegate;privatefinalList<String>events;publicAuditedSender(Senderdelegate,List<String>events){this.delegate=Objects.requireNonNull(delegate,"delegate");this.events=Objects.requireNonNull(events,"events");}publicvoidsend(Stringmessage){events.add("attempt");try{delegate.send(message);}catch(DeliveryFailurefailure){events.add("failed");throwfailure;}events.add("sent");}}publicstaticfinalclassRetryOnceSenderimplementsSender{privatefinalSenderdelegate;publicRetryOnceSender(Senderdelegate){this.delegate=Objects.requireNonNull(delegate,"delegate");}publicvoidsend(Stringmessage){try{delegate.send(message);}catch(DeliveryFailurefailure){delegate.send(message);}}}}
4. Complete test sources
Migrated regression tests retain their original expectations. Structure tests examine the new boundaries and compiled references.
packageedu.se506.lending.regression;importedu.se506.lending.application.ReservationCoordinator;importedu.se506.lending.bootstrap.ReservationApplication;importedu.se506.lending.domain.Reservation;importedu.se506.lending.infrastructure.notification.RecordingNotificationSender;importjava.time.LocalDate;importjava.util.List;importorg.junit.jupiter.api.Test;import staticorg.junit.jupiter.api.Assertions.*;classAvailabilityTddTest{@TestvoidoverlappingEnquiryIsUnavailableWithoutChangingRecordsOrNotifying(){// Arrange: one confirmed CAM1 reservation, ending exclusively on 22 October.RecordingNotificationSenderinbox=newRecordingNotificationSender();ReservationCoordinatorcoordinator=ReservationApplication.create(inbox);LocalDatestart=LocalDate.of(2026,10,20);coordinator.reserve("B1","CAM1",start,2);List<Reservation>recordsBefore=coordinator.reservations();List<Reservation>noticesBefore=inbox.received();// Act: inspect the overlapping interval without requesting a reservation.booleanavailable=coordinator.isAvailable("CAM1",start.plusDays(1),start.plusDays(3));// Assert: the answer and both kinds of state matter.assertAll(()->assertFalse(available,"An overlapping item must be unavailable"),()->assertEquals(recordsBefore,coordinator.reservations()),()->assertEquals(noticesBefore,inbox.received()));}}
packageedu.se506.lending.regression;importedu.se506.lending.application.ReservationCoordinator;importedu.se506.lending.bootstrap.ReservationApplication;importedu.se506.lending.domain.Reservation;importedu.se506.lending.infrastructure.notification.RecordingNotificationSender;importjava.time.LocalDate;importjava.util.List;importorg.junit.jupiter.api.Test;importorg.junit.jupiter.params.ParameterizedTest;importorg.junit.jupiter.params.provider.ValueSource;import staticorg.junit.jupiter.api.Assertions.*;classAvailabilityTest{privatefinalLocalDateday=LocalDate.of(2026,10,20);privatefinalRecordingNotificationSenderinbox=newRecordingNotificationSender();privatefinalReservationCoordinatorc=ReservationApplication.create(inbox);@TestvoidemptyScheduleIsAvailableWithoutAnySideEffect(){assertTrue(c.isAvailable("CAM1",day,day.plusDays(2)));assertTrue(c.reservations().isEmpty());assertTrue(inbox.received().isEmpty());}@TestvoidadjacentPeriodAndDifferentItemAreAvailable(){c.reserve("B1","CAM1",day,2);List<Reservation>records=c.reservations();List<Reservation>notices=inbox.received();assertTrue(c.isAvailable("CAM1",day.plusDays(2),day.plusDays(3)));assertTrue(c.isAvailable("MIC1",day,day.plusDays(2)));assertEquals(records,c.reservations());assertEquals(notices,inbox.received());}@ParameterizedTest@ValueSource(ints={0,-1})voidinvalidPeriodFailsWithoutChangingState(intendOffset){c.reserve("B1","CAM1",day,2);List<Reservation>records=c.reservations();List<Reservation>notices=inbox.received();assertThrows(IllegalArgumentException.class,()->c.isAvailable("CAM1",day,day.plusDays(endOffset)));assertEquals(records,c.reservations());assertEquals(notices,inbox.received());}@TestvoidmalformedIdentifiersAndNullDatesAreRejected(){assertThrows(IllegalArgumentException.class,()->c.isAvailable(" ",day,day.plusDays(1)));assertThrows(IllegalArgumentException.class,()->c.isAvailable(null,day,day.plusDays(1)));assertThrows(NullPointerException.class,()->c.isAvailable("CAM1",null,day.plusDays(1)));assertThrows(NullPointerException.class,()->c.isAvailable("CAM1",day,null));assertTrue(c.reservations().isEmpty());assertTrue(inbox.received().isEmpty());}@TestvoidqueryDoesNotAllocateAnIdentifier(){c.isAvailable("CAM1",day,day.plusDays(1));assertEquals("Confirmed: R1 for B1 on CAM1 [2026-10-20, 2026-10-21).",c.reserve("B1","CAM1",day,1));}@TestvoidavailabilityDoesNotMeanBorrowerEligibility(){for(Stringitem:List.of("CAM1","MIC1","TRI1"))c.reserve("B1",item,day,1);assertTrue(c.isAvailable("CAM2",day,day.plusDays(1)));assertEquals("Rejected: borrower limit or duration.",c.reserve("B1","CAM2",day,1));assertEquals(3,inbox.received().size());}@TestvoidqueryCanSpanMoreDaysThanAStudentMayReserve(){assertTrue(c.isAvailable("CAM1",day,day.plusDays(30)));assertEquals("Rejected: borrower limit or duration.",c.reserve("B1","CAM1",day,30));}}
packageedu.se506.lending.regression;importedu.se506.lending.application.ConfirmationFormatter;importedu.se506.lending.application.ReservationCoordinator;importedu.se506.lending.domain.BorrowerCategory;importedu.se506.lending.domain.BorrowingPolicy;importedu.se506.lending.domain.ReservationRules;importedu.se506.lending.domain.StaffReservationRules;importedu.se506.lending.infrastructure.notification.RecordingNotificationSender;importedu.se506.lending.infrastructure.persistence.InMemoryReservationRepository;import staticorg.junit.jupiter.api.Assertions.*;importjava.time.LocalDate;importjava.util.Map;importorg.junit.jupiter.api.Test;importorg.junit.jupiter.params.ParameterizedTest;importorg.junit.jupiter.params.provider.CsvSource;classBehaviourStrategyTest{// Starting conditional design retained only for comparison in this exercise.privatebooleanconditional(BorrowerCategorycategory,intcount,intdays){if(category==BorrowerCategory.STUDENT){returncount>=0&&count<3&&days>=1&&days<=7;}returncount>=0&&count<5&&days>=1&&days<=14;}@ParameterizedTest@CsvSource({"STUDENT,2,7,true","STUDENT,3,7,false","STUDENT,0,8,false","STUDENT,-1,2,false","STAFF,4,14,true","STAFF,5,14,false","STAFF,0,15,false","STAFF,0,0,false"})voidstrategiesAndConditionalMatchIndependentExamples(BorrowerCategorycategory,intcount,intdays,booleanexpected){varpolicies=Map.of(BorrowerCategory.STUDENT,newReservationRules(),BorrowerCategory.STAFF,newStaffReservationRules());assertEquals(expected,conditional(category,count,days));assertEquals(expected,policies.get(category).mayRequest(count,days));}@TestvoidinjectedPolicyChangesDecisionWithoutChangingCoordinator(){BorrowingPolicytemporaryStaffRule=(count,days)->count>=0&&count<1&&days>=1&&days<=2;varcoordinator=newReservationCoordinator(Map.of(BorrowerCategory.STUDENT,newReservationRules(),BorrowerCategory.STAFF,temporaryStaffRule),newConfirmationFormatter(),newRecordingNotificationSender(),newInMemoryReservationRepository());varstart=LocalDate.of(2026,10,20);assertEquals("Rejected: borrower limit or duration.",coordinator.reserve("T1","CAM1",start,3,BorrowerCategory.STAFF));assertTrue(coordinator.reserve("T1","CAM1",start,2,BorrowerCategory.STAFF).startsWith("Confirmed: R1"));assertEquals("Rejected: borrower limit or duration.",coordinator.reserve("T1","MIC1",start,1,BorrowerCategory.STAFF));assertTrue(coordinator.reserve("S1","MIC1",start,7,BorrowerCategory.STUDENT).startsWith("Confirmed: R2"));}}
packageedu.se506.lending.regression;importedu.se506.lending.application.ReservationCoordinator;importedu.se506.lending.bootstrap.ReservationApplication;importedu.se506.lending.domain.LoanPeriod;importedu.se506.lending.domain.Reservation;importedu.se506.lending.infrastructure.notification.RecordingNotificationSender;importjava.time.DateTimeException;importjava.time.LocalDate;importjava.util.List;publicclassChapter02Checks{privatestaticintchecks;publicstaticvoidmain(String[]args){LocalDateday=LocalDate.of(2026,10,20);LoanPeriodfirst=newLoanPeriod(day,day.plusDays(2));LoanPeriodoverlapping=newLoanPeriod(day.plusDays(1),day.plusDays(3));LoanPeriodadjacent=newLoanPeriod(day.plusDays(2),day.plusDays(3));check(first.overlaps(overlapping),"overlap detected");check(overlapping.overlaps(first),"overlap is symmetric");check(!first.overlaps(adjacent),"adjacent periods allowed");check(!adjacent.overlaps(first),"adjacency symmetric");check(first.overlaps(newLoanPeriod(day,day.plusDays(2))),"equal periods overlap");check(first.overlaps(newLoanPeriod(day.minusDays(1),day.plusDays(4))),"containment overlaps");expect(IllegalArgumentException.class,()->newLoanPeriod(day,day),"zero-length period");expect(IllegalArgumentException.class,()->newLoanPeriod(day,day.minusDays(1)),"reversed period");expect(NullPointerException.class,()->newLoanPeriod(null,day),"null start");expect(NullPointerException.class,()->newLoanPeriod(day,null),"null end");ReservationCoordinatorc=ReservationApplication.create(newRecordingNotificationSender());check(c.reserve("B1","CAM1",day,2).equals("Confirmed: R1 for B1 on CAM1 [2026-10-20, 2026-10-22)."),"exact first confirmation");check(c.reservations().size()==1,"one success stored");check(c.reserve("B2","CAM1",day.plusDays(1),1).equals("Rejected: item unavailable."),"overlapping same item rejected");check(c.reservations().size()==1,"conflict stores nothing");check(c.reserve("B2","CAM1",day.plusDays(2),1).equals("Confirmed: R2 for B2 on CAM1 [2026-10-22, 2026-10-23)."),"adjacent request and identifier after rejection");check(c.reserve("B1","MIC1",day,7).equals("Confirmed: R3 for B1 on MIC1 [2026-10-20, 2026-10-27)."),"different item and seven-day limit allowed");check(c.reserve("B1","TRI1",day,1).startsWith("Confirmed:"),"third active reservation allowed");check(c.reserve("B1","CAM2",day,1).equals("Rejected: borrower limit or duration."),"fourth rejected");check(c.reservations().size()==4,"borrower-limit rejection stores nothing");List<Reservation>snapshot=c.reservations();expect(UnsupportedOperationException.class,snapshot::clear,"snapshot unmodifiable");check(c.reservations().size()==4,"failed clear preserves coordinator state");check(c.reserve("B3","CAM2",day,1).startsWith("Confirmed:"),"another borrower unaffected by B1 limit");check(snapshot.size()==4&&c.reservations().size()==5,"old snapshot does not track later additions");check(snapshot.get(0).period().start().equals(day),"stored period preserved");ReservationCoordinatorinvalid=ReservationApplication.create(newRecordingNotificationSender());check(invalid.reserve("B1","CAM1",day,0).equals("Rejected: borrower limit or duration."),"zero duration rejected");check(invalid.reserve("B1","CAM1",day,-1).equals("Rejected: borrower limit or duration."),"negative duration rejected");check(invalid.reserve("B1","CAM1",day,8).equals("Rejected: borrower limit or duration."),"eight-day duration rejected");expect(IllegalArgumentException.class,()->invalid.reserve(" ","CAM1",day,1),"blank borrower");expect(IllegalArgumentException.class,()->invalid.reserve("B1",null,day,1),"null item");expect(NullPointerException.class,()->invalid.reserve("B1","CAM1",null,1),"null request date");expect(DateTimeException.class,()->invalid.reserve("B1","CAM1",LocalDate.MAX,1),"unrepresentable end date");check(invalid.reservations().isEmpty(),"invalid requests store nothing");expect(IllegalArgumentException.class,()->newReservation("","B1","CAM1",first),"blank reservation identifier");expect(NullPointerException.class,()->newReservation("R1","B1","CAM1",null),"null reservation period");System.out.println("All "+checks+" chapter checks passed.");}privatestaticvoidcheck(booleancondition,Stringlabel){if(!condition){thrownewAssertionError(label);}checks++;}privatestaticvoidexpect(Class<?extendsThrowable>type,Runnableaction,Stringlabel){try{action.run();}catch(Throwableerror){check(type.isInstance(error),label+": wrong exception "+error);return;}thrownewAssertionError(label+": expected "+type.getSimpleName());}}
packageedu.se506.lending.regression;importedu.se506.lending.application.ReservationCoordinator;importedu.se506.lending.bootstrap.ReservationApplication;importedu.se506.lending.infrastructure.notification.RecordingNotificationSender;importjava.time.LocalDate;publicclassChapter03AcceptanceExamples{publicstaticvoidmain(String[]args){LocalDateday=LocalDate.of(2026,10,20);ReservationCoordinatorcoordinator=ReservationApplication.create(newRecordingNotificationSender());// AC-01: an eligible request is stored once and confirmed.Stringaccepted=coordinator.reserve("B1","CAM1",day,2);require(accepted.equals("Confirmed: R1 for B1 on CAM1 [2026-10-20, 2026-10-22)."),"AC-01 confirmation");require(coordinator.reservations().size()==1,"AC-01 one record");System.out.println("AC-01 passed");// AC-02: an overlapping request for the same item changes no records.Stringconflict=coordinator.reserve("B2","CAM1",day.plusDays(1),1);require(conflict.equals("Rejected: item unavailable."),"AC-02 rejection");require(coordinator.reservations().size()==1,"AC-02 unchanged count");require(coordinator.reservations().get(0).borrowerId().equals("B1"),"AC-02 original borrower preserved");System.out.println("AC-02 passed");// AC-03: a booking may start at an existing exclusive end,// or end exactly where an existing booking starts.Stringadjacent=coordinator.reserve("B2","CAM1",day.plusDays(2),1);require(adjacent.equals("Confirmed: R2 for B2 on CAM1 [2026-10-22, 2026-10-23)."),"AC-03 adjacent confirmation");require(coordinator.reservations().size()==2,"AC-03 second record");Stringbefore=coordinator.reserve("B3","CAM1",day.minusDays(1),1);require(before.equals("Confirmed: R3 for B3 on CAM1 [2026-10-19, 2026-10-20)."),"AC-03 request ending at an existing start");require(coordinator.reservations().size()==3,"AC-03 third record");System.out.println("AC-03 passed");// AC-04: count limits span items, not just one equipment item.ReservationCoordinatorlimits=ReservationApplication.create(newRecordingNotificationSender());for(Stringitem:newString[]{"CAM1","MIC1","TRI1"}){require(limits.reserve("B1",item,day,1).startsWith("Confirmed:"),"AC-04 establish three valid reservations");}require(limits.reserve("B1","CAM2",day,1).equals("Rejected: borrower limit or duration."),"AC-04 fourth rejected");require(limits.reservations().size()==3,"AC-04 unchanged count");System.out.println("AC-04 passed");// AC-05: one and seven days are allowed; zero, negative and eight are not.ReservationCoordinatordurations=ReservationApplication.create(newRecordingNotificationSender());require(durations.reserve("B1","CAM1",day,1).startsWith("Confirmed:"),"AC-05 one day");require(durations.reserve("B1","MIC1",day,7).startsWith("Confirmed:"),"AC-05 seven days");for(intdays:newint[]{0,-1,8}){require(durations.reserve("B1","TRI1",day,days).equals("Rejected: borrower limit or duration."),"AC-05 invalid duration");require(durations.reservations().size()==2,"AC-05 unchanged count");}System.out.println("AC-05 passed");System.out.println("All 5 acceptance examples passed.");}privatestaticvoidrequire(booleancondition,Stringdescription){if(!condition){thrownewAssertionError(description);}}}
packageedu.se506.lending.regression;importedu.se506.lending.application.DirectoryReservationService;importedu.se506.lending.application.port.DirectoryException;importedu.se506.lending.bootstrap.ReservationApplication;importedu.se506.lending.domain.Borrower;importedu.se506.lending.domain.BorrowerCategory;importedu.se506.lending.infrastructure.directory.LegacyDirectoryAdapter;importedu.se506.lending.infrastructure.directory.LegacyDirectoryClient;importedu.se506.lending.infrastructure.notification.RecordingNotificationSender;import staticorg.junit.jupiter.api.Assertions.*;importjava.time.LocalDate;importjava.util.Optional;importjava.util.concurrent.atomic.AtomicInteger;importorg.junit.jupiter.api.Test;importorg.junit.jupiter.params.ParameterizedTest;importorg.junit.jupiter.params.provider.CsvSource;importorg.junit.jupiter.params.provider.NullAndEmptySource;importorg.junit.jupiter.params.provider.ValueSource;classDirectoryAdapterTest{privatefinalLocalDatestart=LocalDate.of(2026,10,20);@ParameterizedTest@CsvSource({"S,STUDENT","T,STAFF"})voidmapsKnownActiveCategories(Stringcode,BorrowerCategoryexpected){varadapter=newLegacyDirectoryAdapter(id->newLegacyDirectoryClient.Person(id,code,"Y"));assertEquals(newBorrower("B1",expected),adapter.find("B1").orElseThrow());}@TestvoidabsenceAndInactiveAreEmpty(){assertTrue(newLegacyDirectoryAdapter(id->null).find("B1").isEmpty());assertTrue(newLegacyDirectoryAdapter(id->newLegacyDirectoryClient.Person(id,"S","N")).find("B1").isEmpty());}@ParameterizedTest@CsvSource({"X,Y","S,X","s,Y","S,y"})voidunknownCodesAreErrors(Stringkind,Stringstatus){varadapter=newLegacyDirectoryAdapter(id->newLegacyDirectoryClient.Person(id,kind,status));assertThrows(DirectoryException.class,()->adapter.find("B1"));}@TestvoidmalformedRowsAreErrors(){for(varrow:newLegacyDirectoryClient.Person[]{newLegacyDirectoryClient.Person("OTHER","S","Y"),newLegacyDirectoryClient.Person(null,"S","Y"),newLegacyDirectoryClient.Person("B1",null,"Y"),newLegacyDirectoryClient.Person("B1","S",null),newLegacyDirectoryClient.Person("B1","X","N")}){assertThrows(DirectoryException.class,()->newLegacyDirectoryAdapter(id->row).find("B1"));}}@ParameterizedTest@NullAndEmptySource@ValueSource(strings={" "})voidinvalidIdentifierDoesNotReachProvider(Stringid){varcalls=newAtomicInteger();varadapter=newLegacyDirectoryAdapter(key->{calls.incrementAndGet();returnnull;});assertThrows(IllegalArgumentException.class,()->adapter.find(id));assertEquals(0,calls.get());}@TestvoidtranslatesExpectedFailureAndKeepsCause(){varoriginal=newLegacyDirectoryClient.Failure("offline");varadapter=newLegacyDirectoryAdapter(id->{throworiginal;});varresult=assertThrows(DirectoryException.class,()->adapter.find("B1"));assertSame(original,result.getCause());}@TestvoidunexpectedProgrammingFailureIsNotDisguisedAsAbsence(){varoriginal=newIllegalStateException("bug");varadapter=newLegacyDirectoryAdapter(id->{throworiginal;});assertSame(original,assertThrows(IllegalStateException.class,()->adapter.find("B1")));}@TestvoidcategoryFromDirectoryControlsReservationPolicy(){varnotices=newRecordingNotificationSender();varcoordinator=ReservationApplication.create(notices);vardirectory=newLegacyDirectoryAdapter(id->newLegacyDirectoryClient.Person(id,id.equals("STAFF1")?"T":"S","Y"));varservice=newDirectoryReservationService(directory,coordinator);assertTrue(service.reserve("STAFF1","CAM1",start,10).startsWith("Confirmed: R1"));assertEquals("Rejected: borrower limit or duration.",service.reserve("STUDENT1","MIC1",start,10));assertEquals(1,coordinator.reservations().size());assertEquals("STAFF1",coordinator.reservations().get(0).borrowerId());assertEquals(1,notices.received().size());}@TestvoidabsenceAndOutageDoNotStoreNotifyOrConsumeId(){varnotices=newRecordingNotificationSender();varcoordinator=ReservationApplication.create(notices);varmissing=newDirectoryReservationService(id->Optional.empty(),coordinator);assertEquals("Rejected: borrower not found or inactive.",missing.reserve("B1","CAM1",start,2));varfailing=newDirectoryReservationService(newLegacyDirectoryAdapter(id->{thrownewLegacyDirectoryClient.Failure("offline");}),coordinator);assertThrows(DirectoryException.class,()->failing.reserve("B1","CAM1",start,2));assertTrue(coordinator.reservations().isEmpty());assertTrue(notices.received().isEmpty());varvalid=newDirectoryReservationService(id->Optional.of(newBorrower(id,BorrowerCategory.STUDENT)),coordinator);assertTrue(valid.reserve("B1","CAM1",start,2).startsWith("Confirmed: R1"));}@TestvoidmismatchedDomainBorrowerIsRejectedBeforeMutation(){varcoordinator=ReservationApplication.create(newRecordingNotificationSender());varservice=newDirectoryReservationService(id->Optional.of(newBorrower("OTHER",BorrowerCategory.STAFF)),coordinator);assertThrows(DirectoryException.class,()->service.reserve("B1","CAM1",start,10));assertTrue(coordinator.reservations().isEmpty());}@TestvoidlocalArgumentErrorsPrecedeLookup(){varcalls=newAtomicInteger();varservice=newDirectoryReservationService(id->{calls.incrementAndGet();returnOptional.empty();},ReservationApplication.create(newRecordingNotificationSender()));assertThrows(IllegalArgumentException.class,()->service.reserve("B1"," ",start,2));assertThrows(NullPointerException.class,()->service.reserve("B1","CAM1",null,2));assertEquals(0,calls.get());}}
packageedu.se506.lending.regression;importedu.se506.lending.application.ReservationCoordinator;importedu.se506.lending.application.port.NotificationException;importedu.se506.lending.application.port.NotificationSender;importedu.se506.lending.bootstrap.ReservationApplication;importedu.se506.lending.domain.Reservation;importjava.time.LocalDate;importorg.junit.jupiter.api.Test;importorg.mockito.ArgumentCaptor;import staticorg.junit.jupiter.api.Assertions.*;import staticorg.mockito.Mockito.*;classNotificationMockitoTest{privatefinalLocalDateday=LocalDate.of(2026,10,20);@TestvoidacceptedReservationIsSentOnceWithItsDomainDetails(){NotificationSendersender=mock(NotificationSender.class);ReservationCoordinatorc=ReservationApplication.create(sender);c.reserve("B1","CAM1",day,2);ArgumentCaptor<Reservation>captured=ArgumentCaptor.forClass(Reservation.class);verify(sender).send(captured.capture());Reservationsent=captured.getValue();assertAll(()->assertEquals("R1",sent.id()),()->assertEquals("B1",sent.borrowerId()),()->assertEquals("CAM1",sent.itemId()),()->assertEquals(day,sent.period().start()),()->assertEquals(day.plusDays(2),sent.period().endExclusive()));}@TestvoidrejectedDurationDoesNotNotify(){NotificationSendersender=mock(NotificationSender.class);ReservationCoordinatorc=ReservationApplication.create(sender);assertEquals("Rejected: borrower limit or duration.",c.reserve("B1","CAM1",day,8));assertTrue(c.reservations().isEmpty());verifyNoInteractions(sender);}@TestvoidexpectedNotificationFailureRetainsReservationWithoutRetry(){NotificationSendersender=mock(NotificationSender.class);doThrow(newNotificationException("Expected test failure")).when(sender).send(any(Reservation.class));ReservationCoordinatorc=ReservationApplication.create(sender);assertEquals("Confirmed: R1 for B1 on CAM1 [2026-10-20, 2026-10-22)."+" Notification failed; reservation retained.",c.reserve("B1","CAM1",day,2));assertEquals(1,c.reservations().size());verify(sender,times(1)).send(any(Reservation.class));}}
packageedu.se506.lending.structure;import staticorg.junit.jupiter.api.Assertions.*;importjava.nio.charset.StandardCharsets;importjava.nio.file.Path;importjava.util.ArrayDeque;importjava.util.ArrayList;importjava.util.Deque;importjava.util.HashSet;importjava.util.List;importjava.util.Map;importjava.util.Set;importjava.util.TreeMap;importjava.util.TreeSet;importjava.util.regex.Pattern;importorg.junit.jupiter.api.Test;/** Checks compiled local dependency direction; not reflection or semantic correctness. */classDependencyRulesTest{privatestaticfinalStringBASE="edu.se506.lending.";privatestaticfinalPatternEDGE=Pattern.compile("(?m)^\\s*(edu\\.se506\\.lending\\.[\\w.$]+)\\s+->\\s+([\\w.$]+)\\s+.*$");@TestvoidcompiledProductionDependenciesFollowDeclaredDirection()throwsException{Stringoutput=jdepsOutput();varmatcher=EDGE.matcher(output);intobserved=0;List<String>violations=newArrayList<>();while(matcher.find()){observed++;Stringfrom=matcher.group(1);Stringto=matcher.group(2);if(from.startsWith(BASE+"bootstrap."))continue;if(to.startsWith("patterns.")||to.startsWith("behaviour.")||to.startsWith("examples.")){violations.add(from+" -> "+to);}elseif(to.startsWith(BASE)&&!allowed(from,to)){violations.add(from+" -> "+to);}}assertTrue(observed>0,"jdeps must discover production class dependencies: "+output);assertTrue(violations.isEmpty(),String.join(System.lineSeparator(),violations));}@TestvoidproductionPackagesHaveNoDependencyCycles()throwsException{Map<String,Set<String>>graph=newTreeMap<>();varmatcher=EDGE.matcher(jdepsOutput());while(matcher.find()){Stringfrom=packageOf(matcher.group(1));Stringto=matcher.group(2).startsWith(BASE)?packageOf(matcher.group(2)):null;graph.computeIfAbsent(from,key->newTreeSet<>());if(to!=null&&!to.equals(from))graph.get(from).add(to);}assertTrue(graph.size()>1,"jdeps must discover more than one production package");List<String>cycle=findCycle(graph);assertTrue(cycle.isEmpty(),"Package dependency cycle: "+String.join(" -> ",cycle));}privatestaticStringjdepsOutput()throwsException{Stringexecutable=System.getProperty("os.name").toLowerCase().contains("win")?"java.exe":"java";Stringjava=Path.of(System.getProperty("java.home"),"bin",executable).toString();Processprocess=newProcessBuilder(java,"--module","jdk.jdeps/com.sun.tools.jdeps.Main","-verbose:class","-filter:none",Path.of("target/classes").toAbsolutePath().toString()).redirectErrorStream(true).start();Stringoutput=newString(process.getInputStream().readAllBytes(),StandardCharsets.UTF_8);assertEquals(0,process.waitFor(),output);returnoutput;}privatestaticStringpackageOf(Stringtype){Stringouter=type.contains("$")?type.substring(0,type.indexOf('$')):type;returnouter.substring(0,outer.lastIndexOf('.'));}/** Depth-first search; returns the packages on one cycle, or an empty list. */privatestaticList<String>findCycle(Map<String,Set<String>>graph){Set<String>done=newHashSet<>();for(Stringstart:graph.keySet()){Deque<String>path=newArrayDeque<>();List<String>found=visit(start,graph,path,done);if(!found.isEmpty())returnfound;}returnList.of();}privatestaticList<String>visit(Stringnode,Map<String,Set<String>>graph,Deque<String>path,Set<String>done){if(path.contains(node)){List<String>cycle=newArrayList<>();booleaninCycle=false;for(Stringelement:(Iterable<String>)path::descendingIterator){if(element.equals(node))inCycle=true;if(inCycle)cycle.add(element);}cycle.add(node);returncycle;}if(!done.add(node))returnList.of();path.push(node);for(Stringnext:graph.getOrDefault(node,Set.of())){List<String>found=visit(next,graph,path,done);if(!found.isEmpty())returnfound;}path.pop();returnList.of();}privatebooleanallowed(Stringfrom,Stringto){if(from.startsWith(BASE+"domain."))returnto.startsWith(BASE+"domain.");if(from.startsWith(BASE+"application.port.")){returnto.startsWith(BASE+"application.port.")||to.startsWith(BASE+"domain.");}if(from.startsWith(BASE+"application.")){returnto.startsWith(BASE+"application.")||to.startsWith(BASE+"domain.");}if(from.startsWith(BASE+"infrastructure.")){returnto.startsWith(BASE+"infrastructure.")||to.startsWith(BASE+"application.")||to.startsWith(BASE+"domain.");}if(from.startsWith(BASE+"presentation.")){returnto.startsWith(BASE+"presentation.")||to.startsWith(BASE+"application.")||to.startsWith(BASE+"domain.");}returnfalse;}}
packageedu.se506.lending.structure;import staticorg.junit.jupiter.api.Assertions.*;importedu.se506.lending.application.DirectoryReservationService;importedu.se506.lending.application.port.DirectoryException;importedu.se506.lending.bootstrap.ReservationApplication;importedu.se506.lending.domain.*;importedu.se506.lending.infrastructure.notification.RecordingNotificationSender;importedu.se506.lending.presentation.ReservationConsole;importjava.io.ByteArrayOutputStream;importjava.io.PrintStream;importjava.nio.charset.StandardCharsets;importjava.time.LocalDate;importjava.util.Optional;importorg.junit.jupiter.api.Test;classReservationConsoleTest{@TestvoidprintsExistingSuccessAndRejectionWithoutDuplicatingPolicy(){varbuffer=newByteArrayOutputStream();varcoordinator=ReservationApplication.create(newRecordingNotificationSender());varservice=newDirectoryReservationService(id->Optional.of(newBorrower(id,BorrowerCategory.STUDENT)),coordinator);varconsole=newReservationConsole(service,newPrintStream(buffer,true,StandardCharsets.UTF_8));varstart=LocalDate.of(2026,10,20);console.reserve("B1","CAM1",start,2);console.reserve("B1","MIC1",start,8);assertEquals("Confirmed: R1 for B1 on CAM1 [2026-10-20, 2026-10-22)."+System.lineSeparator()+"Rejected: borrower limit or duration."+System.lineSeparator(),buffer.toString(StandardCharsets.UTF_8));assertEquals(1,coordinator.reservations().size());}@TestvoidstaffRequestAboveTheStudentLimitIsDecidedByThePolicyNotTheConsole(){varbuffer=newByteArrayOutputStream();varcoordinator=ReservationApplication.create(newRecordingNotificationSender());varservice=newDirectoryReservationService(id->Optional.of(newBorrower(id,BorrowerCategory.STAFF)),coordinator);varconsole=newReservationConsole(service,newPrintStream(buffer,true,StandardCharsets.UTF_8));console.reserve("T1","CAM1",LocalDate.of(2026,10,20),10);assertEquals("Confirmed: R1 for T1 on CAM1 [2026-10-20, 2026-10-30)."+System.lineSeparator(),buffer.toString(StandardCharsets.UTF_8));}@TestvoiddirectoryFailureDoesNotPrintSuccess(){varbuffer=newByteArrayOutputStream();varfailure=newDirectoryException("offline");varcoordinator=ReservationApplication.create(newRecordingNotificationSender());varservice=newDirectoryReservationService(id->{throwfailure;},coordinator);varconsole=newReservationConsole(service,newPrintStream(buffer,true,StandardCharsets.UTF_8));assertSame(failure,assertThrows(DirectoryException.class,()->console.reserve("B1","CAM1",LocalDate.of(2026,10,20),2)));assertEquals("",buffer.toString(StandardCharsets.UTF_8));assertTrue(coordinator.reservations().isEmpty());}}
#!/usr/bin/env bash
set -euo pipefail
cd"$(dirname "${BASH_SOURCE[0]}")/..""${CHAPTER_MVN:-mvn}" --batch-mode verify
5.4 after/.gitignore
/target/
/build/
*.class
5.4a after/.gitattributes
*.sh text eol=lf
This is the Chapter 4 file that keeps scripts/check.sh in LF form when Git on Windows converts line endings. Without it, a CRLF copy of the script fails under WSL or Linux bash.
5.4b after/tools/PackageMetrics.java
importjava.nio.charset.StandardCharsets;importjava.nio.file.Path;importjava.util.Map;importjava.util.Set;importjava.util.TreeMap;importjava.util.TreeSet;importjava.util.regex.Matcher;importjava.util.regex.Pattern;/**
* Prints afferent coupling (Ca), efferent coupling (Ce) and instability
* I = Ce / (Ca + Ce) for each package of the compiled production classes.
*
* Counting unit: classes. Nested classes count with their outer class. Only
* dependencies between this project's own packages are counted; the standard
* library is ignored. When Ca + Ce is 0 the instability is undefined.
*
* Run from the project root after "mvn compile":
* java tools/PackageMetrics.java target/classes
*/publicfinalclassPackageMetrics{privatestaticfinalPatternEDGE=Pattern.compile("(?m)^\\s+([\\w.$]+)\\s+->\\s+([\\w.$]+)\\s+\\S+\\s*$");publicstaticvoidmain(String[]args)throwsException{if(args.length!=1){System.err.println("usage: java tools/PackageMetrics.java <classes directory>");System.exit(2);}Stringexecutable=System.getProperty("os.name").toLowerCase().contains("win")?"java.exe":"java";Stringjava=Path.of(System.getProperty("java.home"),"bin",executable).toString();Processprocess=newProcessBuilder(java,"--module","jdk.jdeps/com.sun.tools.jdeps.Main","-verbose:class","-filter:none",Path.of(args[0]).toAbsolutePath().toString()).redirectErrorStream(true).start();Stringoutput=newString(process.getInputStream().readAllBytes(),StandardCharsets.UTF_8);if(process.waitFor()!=0){System.err.println(output);System.exit(1);}Map<String,Set<String>>classesIn=newTreeMap<>();Map<String,Set<String>>dependersOutside=newTreeMap<>();// classes outside P that use PMap<String,Set<String>>dependersInside=newTreeMap<>();// classes in P that use outside classesMatcherm=EDGE.matcher(output);while(m.find()){Stringfrom=outer(m.group(1));Stringto=outer(m.group(2));if(!local(from))continue;classesIn.computeIfAbsent(pkg(from),k->newTreeSet<>()).add(from);if(!local(to))continue;classesIn.computeIfAbsent(pkg(to),k->newTreeSet<>()).add(to);if(!pkg(from).equals(pkg(to))){dependersOutside.computeIfAbsent(pkg(to),k->newTreeSet<>()).add(from);dependersInside.computeIfAbsent(pkg(from),k->newTreeSet<>()).add(from);}}System.out.printf("%-48s %7s %4s %4s %9s%n","package","classes","Ca","Ce","I");for(Stringp:classesIn.keySet()){intca=dependersOutside.getOrDefault(p,Set.of()).size();intce=dependersInside.getOrDefault(p,Set.of()).size();Stringinstability=ca+ce==0?"undefined":String.format("%.2f",ce/(double)(ca+ce));System.out.printf("%-48s %7d %4d %4d %9s%n",p,classesIn.get(p).size(),ca,ce,instability);}}privatestaticbooleanlocal(Stringtype){returntype.startsWith("edu.se506.")||type.startsWith("patterns.")||type.startsWith("behaviour.")||type.startsWith("examples.");}privatestaticStringouter(Stringtype){intdollar=type.indexOf('$');returndollar<0?type:type.substring(0,dollar);}privatestaticStringpkg(Stringtype){returntype.substring(0,type.lastIndexOf('.'));}}
5.5 after/README.md
# Chapter 10 checkpoint
Use a full Java 17 JDK and Maven 3.9.11. The JDK must include jdk.jdeps for the compiled dependency test. Existing dependency and plugin versions are unchanged.
From this directory:
```bash
mvn --batch-mode verify
java -cp target/classes edu.se506.lending.bootstrap.Chapter10Demo
java -cp target/classes edu.se506.lending.bootstrap.Chapter09Demo
java -cp target/classes edu.se506.lending.bootstrap.Chapter08Demo
java -cp target/classes edu.se506.lending.bootstrap.Chapter07Demo
```The full build executes 121 JUnit invocations: 107 migrated inherited cases, seven repository tests, two value-semantics tests, three console tests and two compiled dependency tests (direction and package cycles). Four inherited cases bridge older plain-Java runners. To print the package metrics used in the chapter, run `mvn --batch-mode compile` and then `java tools/PackageMetrics.java target/classes`.
Production classes now live under edu.se506.lending. Bootstrap chooses concrete implementations. Application and domain do not import infrastructure. Application ports depend only on domain and other ports. Infrastructure may depend on application, domain and infrastructure; presentation may depend on application, domain and presentation. Bootstrap may assemble all of them and launch explicitly isolated teaching examples.
ReservationCoordinator now requires a ReservationRepository as its fourth constructor argument. ReservationApplication.create(sender) retains its method shape in its new package and supplies a fresh InMemoryReservationRepository. Existing source/binary compatibility and old unqualified launcher names are not retained.
The existing result strings, policies, loan lifecycle and notification timing remain unchanged. The new console prints those existing results. Repository extraction does not introduce persistence, concurrency safety or transactionality.
The patterns and behaviour packages remain isolated examples. examples.structure.PeriodValue demonstrates record equality without replacing the cumulative LoanPeriod. ADR-010 is included in docs; its status is implemented teaching example, peer review pending.
5.6 after/docs/ADR-010-application-structure.md
# ADR-010: Application structure and reservation storage boundary
Date: 13 October 2026
Status: Implemented teaching example; peer review pending.
## Context
The Chapter 9 application has useful class boundaries but mostly unnamed-package production code. ReservationCoordinator owns a concrete list. Concrete construction, storage ownership and permitted dependencies are not sufficiently visible for the growing example.
The change must retain the existing sequential use-case outcomes, policy limits, output strings, directory errors, notification timing and loan behaviour. Java package names, imports, launcher names and direct construction may change if documented.
## Decision
Use edu.se506.lending packages for domain, application, application.port, infrastructure, presentation and bootstrap. Application-owned ports express the directory, sender and reservation store contracts. Domain code has no dependencies on the other application packages. Bootstrap selects concrete adapters.
Extract ReservationRepository with findAll and add only. Its in-memory implementation provides insertion-ordered, stable unmodifiable snapshots and rejects null or duplicate IDs without replacing records. The coordinator receives the port as a fourth constructor argument. The usual factory supplies one fresh repository per coordinator/session.
Retain existing domain equality, size-based IDs, string results and loan storage. Keep the separate pattern/lifecycle exercises isolated. Add a thin console adapter and compiled dependency checks, for direction and for package cycles, using the JDK jdeps tool.
## Alternatives considered
- Keep the unnamed package and private list: less code, but weaker visibility of ownership and dependency direction as the example grows.
- Use narrow packages and one storage port: selected because it makes the current boundaries explicit without a framework.
- Introduce a generic CRUD repository, ORM, database or DI framework: declined because those capabilities are not required by this change.
## Consequences
Existing use-case outcomes remain protected by regression tests. Callers must update imports, direct coordinator construction and Java launcher names. The dependency test requires a full Java 17 JDK with jdk.jdeps. More files and one additional storage contract must be maintained.
The application remains sequential and in memory. A repository interface does not provide transactions, durable storage or concurrency safety. The factory's fresh-store ownership is important for the existing identifier scheme. Arbitrary preloaded/shared stores are not promised by this checkpoint.
ConfirmationFormatter remains application-owned to preserve result text. The console notification adapter may use it. Future structured results require a separate reviewed API decision.
## Validation and review
Run mvn --batch-mode verify, compare the earlier demonstration transcripts under their new fully qualified names, inspect compiled dependency directions and package cycles, and review repository ordering/snapshot/duplicate-ID behaviour.
Local author checks are reported in the chapter companion. No hosted CI run, opened PR or independent peer review is claimed. A learner's review must record actual feedback and its resolution.
## Revisit triggers
Persistent storage; multiple writers; a cancellation lifecycle; a new identifier scheme; adoption of value equality in the cumulative period; or structured application results. Each requires a concrete behaviour and compatibility analysis before implementation.
6. Worked explanation and evidence
6.1 The storage extraction
The Chapter 9 starting point was a coordinator-owned list. Chapter 10 replaces that field with a ReservationRepository reference and redirects reads and append through the port. Policy evaluation, date validation, conflict meaning, identifier calculation and notification handling stay in the coordinator.
The in-memory implementation uses a LinkedHashMap so duplicate-ID detection and ordered snapshots have a clear implementation. Duplicate keys are rejected rather than replacing the original record. The adapter does not calculate borrower limits or trigger notifications.
The standard factory preserves the previous ownership model by creating a new repository for each coordinator. The independent-session test verifies that reserving in one session neither populates another nor consumes its R1. The constructor injection point does not itself define a shared or persistent storage lifecycle.
6.2 Source direction versus runtime calls
ReservationCoordinator imports the application port. InMemoryReservationRepository implements that port. Bootstrap imports both the coordinator and the concrete adapter. At runtime, interface dispatch reaches the adapter selected by bootstrap; source dependency still points towards the port.
The infrastructure notification implementation also refers to ConfirmationFormatter in application. This is permitted by the declared matrix and preserves the current text contract. A review should describe that concrete dependency, rather than assuming every infrastructure reference goes only to an interface.
The domain classes do not import application or infrastructure. The compiled dependency test checks this local direction using jdeps output. It also prevents ordinary application code from importing an isolated pattern exercise. A bootstrap demo may refer to one explicitly, which is why the exception is narrow and documented.
6.3 The new presentation adapter
ReservationConsole delegates a request to DirectoryReservationService and prints the existing result. Its tests supply a PrintStream backed by a byte buffer. They observe one confirmation and one duration rejection, then verify that only the accepted request was stored.
A separate test makes the directory fail. The same exception propagates and the output buffer remains empty. The console does not manufacture a success message or silently substitute a category.
The class is intentionally small. It does not read input, manage authentication, catch every exception or implement a graphical interface. Those would be new product requirements. Bootstrap remains responsible for selecting examples and collaborators.
6.4 Equality, identity and snapshot observations
The repository duplicate-ID test creates two distinct Reservation objects with the same identifier and different item data. The second add rejects, and the original object remains stored. This uses explicit domain identity rather than relying on Reservation.equals.
ValueSemanticsTest makes the separate PeriodValue record’s behaviour visible through equals, hashCode and a set. It also deliberately checks that running LoanPeriod objects with equal dates remain unequal under their inherited Java equality. That regression prevents the structural migration from silently adopting a new equality policy.
The repository snapshot test takes an empty snapshot, appends one record, takes another snapshot, and appends a second record. Both earlier snapshots remain unchanged, the new snapshot preserves insertion order, and mutation through the returned list is rejected. This promise relies on immutable values as well as an unmodifiable list.
6.5 New test inventory
Test class
Invocations
Purpose
RepositoryBoundaryTest
7
Snapshot semantics, duplicate/null rejection, storage-before-notification, failed add, independent sessions and a required repository
ValueSemanticsTest
2
Separate record equality and preserved running period equality
ReservationConsoleTest
3
Existing output contract, a staff request above the student limit, and unhidden directory failure
DependencyRulesTest
2
Compiled production references against the local package matrix; no package cycle
Migrated earlier suite
107
Existing policies, directory, loan, notification, pattern and lifecycle examples
Total
121
Complete version gate
Three of these tests exist because the fault experiment in Section 11.2 found gaps. coordinatorRequiresARepository pins the constructor’s null check. staffRequestAboveTheStudentLimitIsDecidedByThePolicyNotTheConsole sends a ten-day staff request through the console: a console test that used only a student would let a console that quietly enforced the seven-day rule itself pass. productionPackagesHaveNoDependencyCycles closes the gap that the layer matrix leaves inside one layer.
The failing-add example throws before storing anything. It establishes that notification follows a successful add in this implementation path. It does not establish the outcome of a database write whose acknowledgement is lost. Likewise, the architecture check establishes observed static relationships, not semantic correctness or runtime transactionality.
7. Suggested practical solution
7.1 A reviewable migration sequence
Start from the tested Chapter 9 version. Identify each class’s responsibility before moving it. Introduce the package layout and update imports and launcher names. Keep the separate pattern and lifecycle exercises separate.
Extract the reservation storage port with the minimum operations needed by the coordinator. Move the concrete collection into its adapter. Update bootstrap and direct test constructions to pass that adapter explicitly. Avoid an application constructor that secretly creates infrastructure to retain the old signature.
Run the inherited tests with their expectations unchanged. Then add the repository contract tests, presentation tests and dependency gate. Compare earlier demonstration transcripts under their new launcher names. Write the decision record and ask a reviewer to inspect both behaviour preservation and dependency ownership.
The supplied files are a completed example of that organisation. They are not a claim that a particular Git history or peer review already exists. A learner’s submission should retain its own actual diffs, commands and feedback.
7.2 Suggested architecture decision
The complete ADR-010 is included among the file listings. Its status is implemented teaching example, peer review pending. It records the unnamed-package and coordinator-owned-list starting point, the chosen narrow boundaries, alternatives declined and compatibility costs.
A valid alternative could keep the list inside the coordinator for a permanently small program. Another could place a repository contract in domain if a richer domain model genuinely owns that need. Evaluate those alternatives against actual responsibilities rather than treating the selected package name as the only valid answer.
The worked decision declines a database, generic CRUD framework and dependency-injection framework. Those choices avoid making storage and deployment commitments that the requirement did not request. They do not prevent a later reviewed change.
7.3 Draft PR description for learners to adapt
Status: sample text only; no PR has been opened by this chapter.
The reservation use case has grown beyond a useful unnamed-package layout, and its coordinator directly owns concrete storage. This change introduces named responsibility packages, an application-owned reservation repository port and an in-memory adapter assembled at bootstrap.
Existing policies, result strings, directory errors, loan behaviour and notification timing remain protected by regression tests. Callers must update imports, direct coordinator construction and fully qualified launcher names. The change adds a thin console adapter and a compiled dependency check.
Validation includes the full Maven suite, repository contract checks, existing demonstration comparisons and inspection of the declared dependency directions. The example remains sequential and in memory; persistence, cancellation, identifier redesign and structured result objects are outside this change.
Review should focus on ownership of the port, preservation of storage-before-notification ordering, snapshot semantics and whether each allowed dependency has a concrete reason.
Replace validation statements with the results for the actual submitted revision. Do not present this draft as an independent review or a hosted CI run.
7.4 Model answers: the book’s project steps B1–B3
B1. Classify.
Class
Package
May refer to
Must not refer to
StaffReservationRules
domain
domain
Any application, infrastructure or presentation class, for example NotificationSender
LegacyDirectoryAdapter
infrastructure.directory
domain, application (including its ports) and infrastructure
presentation, bootstrap or an isolated example
DirectoryReservationService
application
domain, application.port and application
Any infrastructure class, for example the adapter it receives through its constructor
InMemoryReservationRepository
infrastructure.persistence
domain, application.port and infrastructure
presentation or bootstrap
ReservationApplication
bootstrap
Every application package, and explicitly demonstrated examples
Nothing in the production matrix is forbidden, but it should hold no business rule
B2. Check the references. A permitted reference in the compiled report is infrastructure.directory.LegacyDirectoryAdapter -> application.port.BorrowerDirectory: infrastructure may refer to the application’s ports, and the adapter implements that contract. The reference that runs from infrastructure.notification to application is ConsoleNotificationSender -> application.ConfirmationFormatter. It exists because the console sender builds its notice text with the shared formatter, which preserves the existing wording.
B3. Diagnose the package design.
(a) Two possible change requests. The console should print the borrower’s name: presentation only, if the service result already carries it, otherwise application too, which shows the string-result limitation. Add an email notifier: a new class in infrastructure.notification and one construction change in bootstrap; the coordinator and the domain are untouched. Accept any request for which the packages are named and the reason is given.
(b) The arrow is infrastructure.notification (I = 0.30) to application (I = 0.31). The difference is too small to act on by itself. What the arrow points at is the concrete dependency on ConfirmationFormatter. Reasonable options: leave it, because the formatter’s output is part of the observable result; or introduce a small formatting port in application.port so that the sender depends only on a contract, at the cost of another interface. Either answer is acceptable if it names the trade-off.
(c) ConfirmationFormatter should stay in application. The coordinator needs it to build the returned text, so moving it into infrastructure.notification would make application depend on infrastructure, which is the same failure as moving NotificationException (Section 10.5 of the chapter). Moving it into domain would put wording in the domain. What would change the answer: a second notification format that needs its own formatter, or the dependency report showing more concrete application classes referenced from infrastructure.
Experiment behind Option B in the chapter.NotificationException was moved to infrastructure.notification in a copy and its imports changed. The direction test failed with application.ReservationCoordinator -> infrastructure.notification.NotificationException, and the cycle test failed with application -> infrastructure.notification -> application. Both tests together reject the placement.
7.5 Guidance for your team’s project steps T1–T5
These are not model answers, because the content is your team’s. They describe what a good answer shows.
T1 diagram. Boxes are groups of packages, arrows are permitted references, and every arrow can be justified by one sentence. A good diagram marks at least one forbidden reference, and the dependency check or import review confirms that it does not occur. A weak diagram copies Figure 10.1 without checking it against the team’s code.
T2 decision record. The context names a problem someone could observe (for example, “the checkout class creates the payment client and cannot be tested”). The alternatives include doing nothing and say why it was rejected or accepted. The revisit trigger is an event, such as “when a second storage mechanism is needed”.
T3 restructuring. Package moves and logic changes are separate commits or clearly separate parts of the diff, and the existing checks pass before and after.
T4 dependency evidence. It says whether the check was compiled or manual, lists forbidden references found or states none were, and states whether a cycle search was made.
T5 review. It records a real reviewer’s comments and the response, and the Milestone 3 checklist status is honest about gaps.
8. AI activity solution
Fictional claim
Finding
Evidence to inspect
A runtime call to the in-memory store proves an application import of infrastructure
Incorrect: interface dispatch and source references are different relationships
Coordinator’s port field, bootstrap construction, jdeps edge
Put the repository interface in infrastructure
Not justified by this use case; it would make the consuming application depend on an infrastructure-owned contract
Port ownership and the declared dependency matrix
Every entity needs repository and service classes
No such requirement follows from identity
Actual access and orchestration needs; LoanPeriod has neither
An unmodifiable list makes every element immutable
Incorrect for mutable elements
List contract plus Reservation/LoanPeriod field semantics
Bootstrap must not import adapters
Incorrect for the selected composition role
Explicit assembly and absence of concrete construction in application
Passing tests establishes peer review
Incorrect evidence substitution
Actual reviewer comments and their resolution
A dependency review should name the referencing type and referenced type. “Infrastructure coupling” without that trace is a claim to investigate. Conversely, legal imports do not prove appropriate responsibilities: a console can duplicate a policy using only standard Java types and still violate the intended design.
The offline example is author-written fictional AI output. If a live assistant is used, retain relevant actual output, independently inspect references and record accepted/rejected claims. No account or paid service is needed for the supplied exercise.
9. Intentionally flawed alternatives
These fragments are diagnostic alternatives, not additional files to compile alongside the final version. Each of them corresponds to a fault that was run in the experiment in Section 11.2.
9.1 Construct infrastructure inside the coordinator
// WRONG for this chapter's declared application dependency rule:privatefinalReservationRepositoryrepository=newInMemoryReservationRepository();
Even if reservation behaviour appears unchanged, the application now names a concrete infrastructure type. The dependency check is designed to reject that compiled edge. Bootstrap should construct the implementation and pass the application-owned interface.
9.2 Expose mutable repository storage
// WRONG for the stable, unmodifiable snapshot contract:publicList<Reservation>findAll(){returnbackingList;}
Callers could change the store without its add checks, and later additions would appear through previously returned views. The final snapshot test checks both non-mutation and historical independence. An unmodifiable view of a live mutable list would still fail the latter promise.
9.3 Replace instead of reject duplicate identity
// WRONG for append-only duplicate-ID rejection if used without the preceding check:records.put(reservation.id(),reservation);
A second object with the same ID would replace the first. The final implementation checks for the existing key and rejects without mutation. If updates become a requirement, define their meaning and concurrency contract explicitly rather than changing this line under the name of refactoring.
9.4 Enforce the policy in the console
// WRONG: the console now owns a policy that the domain already decides.if(days>7){output.println("Rejected: borrower limit or duration.");return;}
A student request over seven days prints the right text, so the original console test cannot tell this from the correct console. A staff request of ten days, which the staff policy allows, is wrongly rejected. The console test in the final suite sends exactly that request.
10. Review answers and transfer solution
10.1 Concept answers
1. Runtime and source direction: the coordinator refers to ReservationRepository. Bootstrap supplies an object implementing that interface. At runtime a call reaches the concrete method through dispatch; the coordinator need not import the implementation. Static and runtime diagrams answer different questions.
2. Service, policy and repository: an application service coordinates a use case across collaborators. A domain policy expresses a rule such as count/duration eligibility. A repository provides access to stored domain records through a stated contract. None of these labels requires a remote service or a generic framework.
3. Identity and equality: R1 identifies the intended reservation independently of its current data. Immutable fields do not remove that identity. Java equality is a separate implementation choice; this Reservation class retains object equality, so storage keys by its explicit identifier. Value equality must likewise be implemented and tested, as the separate PeriodValue example demonstrates.
4. Dependency evidence: jdeps reports compiled class relationships, and the tests compare local edges with the selected matrix and search the package graph for cycles. They do not prove responsibilities are well chosen, inspect reflection, class names held in strings or configuration, or establish transactionality. Behaviour tests and review remain necessary.
10.2 Applied diagnosis answers
5. Duplicated presentation policy: the console and API can disagree if only one contains the copied seven-day check. Put the rule in the domain policy and delegate both entry points to the same application operation. A test can send the same student request through both entry points and compare outcomes and stored state. Also test an allowed staff duration to expose an over-general console rule that rejects every request above seven days.
6. Duplicate IDs do not prevent concurrent booking conflicts: two requests can both observe a free interval and then append different IDs. Even unique identifiers do not make the preceding eligibility/conflict checks and insertion atomic. Define a transaction, locking or constraint strategy appropriate to the real store and test the relevant concurrent outcome before claiming safety. That implementation is beyond this sequential version.
7. Stability numbers at the extremes:bootstrap has I = 1.00 because nothing depends on it, so it can change freely; that is appropriate for the place that chooses concrete classes. Changing it disturbs nobody else, but it must not accumulate business rules, which would then be hard to reuse or test. domain has I = 0.00 because sixteen classes depend on it and it depends on nothing else in the application. A change to it can ripple into every other package, so it is kept small and free of mechanisms. Neither number is a defect.
8. Cycle between two infrastructure packages: the cycle test catches it, because it searches the package graph. The direction test does not: the matrix lets infrastructure packages refer to each other, so each reference is individually permitted. The fault experiment in Section 11.2 confirms this: the direction test alone let the cycle pass.
10.3 Clinic appointment transfer
Keep appointment times, patient identity and eligibility rules in the domain vocabulary. Put the booking operation in application, with a patient-directory port and a focused appointment-store port as needed. Infrastructure adapts the provider and implements in-memory storage. Presentation delegates booking and presents its result; bootstrap selects concrete collaborators.
An appointment has an explicit identity, while its scheduled interval is a value. Do not assume those concepts automatically provide Java equals/hashCode. Specify the interval’s validation and equality and the store’s duplicate-ID behaviour.
The store contract belongs with the layer that defines its required capabilities. An application-owned port is a reasonable starting point for this small booking use case. Avoid generic operations that imply unagreed cancellation or modification rules.
One preserved behaviour is that a conflicting interval is rejected without storing another appointment or notifying anyone. Compare that outcome before and after restructuring. Provider failure versus missing patient is another useful distinction to preserve. No deployment services or real patient records are needed to demonstrate the design.
11. Instructor notes
11.1 Suggested demonstration and review
Show the coordinator’s imports before drawing the runtime sequence. Ask students to identify where the concrete repository is selected. Then inspect the repository contract and the snapshot test. Use the duplicate-ID example to distinguish identity from Java object equality.
Run the thin console demonstration and the complete build. Ask students which test would detect a misplaced concrete dependency and which would detect altered overlap behaviour. Neither answer should rely only on a green summary line.
For Milestone 3, use the module leader’s approved requirements. The chapter supplies a sample decision and PR description, but learners must obtain and record actual peer review. The sample is an example: it has no real reviewer and no hosted CI run behind it.
**The book’s project and your team’s project. Keep the two apart in class. The supplied version, ADR-010 and the sample PR description are the book’s project: students read, trace and diagnose them (steps B1–B3) and are not asked to rebuild them. Milestone 3 evidence comes from each team’s own repository (steps T1–T5). A team that copies ADR-010’s decision has not shown its own architectural judgement; ask what problem in their code the decision solves. A team on another provided domain applies the same steps to that domain’s starter codebase and uses the transfer challenge for orientation.
Teaching the package-design material. Start from the five change requests in Section 10.5 of the chapter and ask students to predict the packages that change before showing the answers. Then show the metrics table and ask which arrow is the odd one out. Resist the urge to turn the numbers into a score: the point is to decide what to examine. Show the NotificationException experiment only after students have argued for Option A or Option B.
11.2 Do the tests detect faults?
Twenty-five faults were introduced one at a time into copies of the project, and each copy was run through mvn test. They were first run before three tests were added: twenty were caught and five survived. The survivors were then run against the final tests.
Area
Faults tried
Result before the three added tests
Result with the final tests
Repository contract
Duplicate id replaces the original; modifiable snapshot copy; live snapshot view; HashMap instead of insertion order
All four caught by RepositoryBoundaryTest or the inherited checks
Unchanged
Repository contract
Explicit null check removed
Survived: removing it still throws NullPointerException, from reservation.id(), so only the message differs
Survives: equivalent behaviour, deliberately not pinned by a message test
Coordinator
Stores after notification; reservations() returns a modifiable copy; identifier skips a number; conflict check ignores stored records
All four caught
Unchanged
Coordinator
Null repository accepted
Survived: no test constructed a coordinator without a repository
Caught by coordinatorRequiresARepository
Assembly
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, for whom the duplicated rule gives the same output
Caught by staffRequestAboveTheStudentLimitIsDecidedByThePolicyNotTheConsole
Value objects
PeriodValue validation removed; LoanPeriod gains value equality
Both caught
Unchanged
Dependency direction
Application builds the concrete repository; domain refers to application; presentation refers to infrastructure; port refers to application; application refers to an isolated example; persistence refers to presentation; application refers to bootstrap
All seven caught by the direction test
Unchanged
Dependency cycle
infrastructure.directory and infrastructure.notification refer to each other
Survived: each reference is individually permitted by the matrix
Caught by productionPackagesHaveNoDependencyCycles
Dependency evasion
A class name held in a string
Survived: jdeps reads compiled references, not strings
Survives: a documented limit; review and behaviour tests must cover it
Of the 25 faults, 20 were caught before the three tests were added, and 23 are caught by the final tests. The two that still survive are an equivalent mutant and a known limit of any bytecode-based dependency check. Different chapters used different fault sets, so these counts say nothing about how this suite compares with earlier ones. The seven-day-rule survivor and the cycle survivor are the most instructive: both tests were green, and both gaps were visible only when a plausible wrong implementation was tried.
Four inherited JUnit cases bridge to earlier plain-Java runners, and their counts remain distinct from JUnit invocations. Do not add them as if every unit were the same kind of test.
The results establish the supplied local scenarios and dependency rules. They do not establish persistence, authentication, concurrent consistency or universal behavioural equivalence for unseen consumers.
References
Online references 1–5 checked 12 October 2026 and 7–8 on 13 October 2026; see the student chapter for source context.
Martin, R. C., Agile Software Development: Principles, Patterns, and Practices. Prentice Hall, 2002. The Week 9 reading is its principles of package design. This book treats package design in Section 10.4 of Chapter 10.