Library
A comprehensive Domain-Driven Design example with problem space strategic analysis and various tactical patterns.
Install / Use
/learn @ddd-by-examples/LibraryREADME
Table of contents
- About
- Domain description
- General assumptions
3.1 Process discovery
3.2 Project structure and architecture
3.3 Aggregates
3.4 Events
3.4.1 Events in Repositories
3.5 ArchUnit
3.6 Functional thinking
3.7 No ORM
3.8 Architecture-code gap
3.9 Model-code gap
3.10 Spring
3.11 Tests - How to contribute
- References
About
This is a project of a library, driven by real business requirements. We use techniques strongly connected with Domain Driven Design, Behavior-Driven Development, Event Storming, User Story Mapping.
Domain description
A public library allows patrons to place books on hold at its various library branches. Available books can be placed on hold only by one patron at any given point in time. Books are either circulating or restricted, and can have retrieval or usage fees. A restricted book can only be held by a researcher patron. A regular patron is limited to five holds at any given moment, while a researcher patron is allowed an unlimited number of holds. An open-ended book hold is active until the patron checks out the book, at which time it is completed. A closed-ended book hold that is not completed within a fixed number of days after it was requested will expire. This check is done at the beginning of a day by taking a look at daily sheet with expiring holds. Only a researcher patron can request an open-ended hold duration. Any patron with more than two overdue checkouts at a library branch will get a rejection if trying a hold at that same library branch. A book can be checked out for up to 60 days. Check for overdue checkouts is done by taking a look at daily sheet with overdue checkouts. Patron interacts with his/her current holds, checkouts, etc. by taking a look at patron profile. Patron profile looks like a daily sheet, but the information there is limited to one patron and is not necessarily daily. Currently a patron can see current holds (not canceled nor expired) and current checkouts (including overdue). Also, he/she is able to hold a book and cancel a hold.
How actually a patron knows which books are there to lend? Library has its catalogue of books where books are added together with their specific instances. A specific book instance of a book can be added only if there is book with matching ISBN already in the catalogue. Book must have non-empty title and price. At the time of adding an instance we decide whether it will be Circulating or Restricted. This enables us to have book with same ISBN as circulated and restricted at the same time (for instance, there is a book signed by the author that we want to keep as Restricted)
General assumptions
Process discovery
The first thing we started with was domain exploration with the help of Big Picture EventStorming.
The description you found in the previous chapter, landed on our virtual wall:

The EventStorming session led us to numerous discoveries, modeled with the sticky notes:

During the session we discovered following definitions:

This made us think of real life scenarios that might happen. We discovered them described with the help of
the Example mapping:

This in turn became the base for our Design Level sessions, where we analyzed each example:

Please follow the links below to get more details on each of the mentioned steps:
Project structure and architecture
At the very beginning, not to overcomplicate the project, we decided to assign each bounded context to a separate package, which means that the system is a modular monolith. There are no obstacles, though, to put contexts into maven modules or finally into microservices.
Bounded contexts should (amongst others) introduce autonomy in the sense of architecture. Thus, each module encapsulating the context has its own local architecture aligned to problem complexity. In the case of a context, where we identified true business logic (lending) we introduced a domain model that is a simplified (for the purpose of the project) abstraction of the reality and utilized hexagonal architecture. In the case of a context, that during Event Storming turned out to lack any complex domain logic, we applied CRUD-like local architecture.

If we are talking about hexagonal architecture, it lets us separate domain and application logic from frameworks (and infrastructure). What do we gain with this approach? Firstly, we can unit test most important part of the application - business logic - usually without the need to stub any dependency. Secondly, we create ourselves an opportunity to adjust infrastructure layer without the worry of breaking the core functionality. In the infrastructure layer we intensively use Spring Framework as probably the most mature and powerful application framework with an incredible test support. More information about how we use Spring you will find here.
As we already mentioned, the architecture was driven by Event Storming sessions. Apart from identifying contexts and their complexity, we could also make a decision that we separate read and write models (CQRS). As an example you can have a look at Patron Profiles and Daily Sheets.
Aggregates
Aggregates discovered during Event Storming sessions communicate with each other with events. There is a contention, though, should they be consistent immediately or eventually? As aggregates in general determine business boundaries, eventual consistency sounds like a better choice, but choices in software are never costless. Providing eventual consistency requires some infrastructural tools, like message broker or event store. That's why we could (and did) start with immediate consistency.
Good architecture is the one which postpones all important decisions
... that's why we made it easy to change the consistency model, providing tests for each option, including basic implementations based on DomainEvents interface, which can be adjusted to our needs and toolset in future. Let's have a look at following examples:
-
Immediate consistency
def 'should synchronize Patron, Book and DailySheet with events'() { given: bookRepository.save(book) and: patronRepo.publish(patronCreated()) when: patronRepo.publish(placedOnHold(book)) then: patronShouldBeFoundInDatabaseWithOneBookOnHold(patronId) and: bookReactedToPlacedOnHoldEvent() and: dailySheetIsUpdated() } boolean bookReactedToPlacedOnHoldEvent() { return bookRepository.findBy(book.bookId).get() instanceof BookOnHold } boolean dailySheetIsUpdated() { return new JdbcTemplate(datasource).query("select count(*) from holds_sheet s where s.hold_by_patron_id = ?", [patronId.patronId] as Object[], new ColumnMapRowMapper()).get(0) .get("COUNT(*)") == 1 }Please note that here we are just reading from database right after events are being published
Simple implementation of the event bus is based on Spring application events:
@AllArgsConstructor public class JustForwardDomainEventPublisher implements DomainEvents { private final ApplicationEventPublisher applicationEventPublisher; @Override public void publish(DomainEvent event) { applicationEventPublisher.publishEvent(event); } } -
Eventual consistency
def 'should synchronize Patron, Book and DailySheet with events'() { given: bookRepository.save(book) and: patronRepo.publish(patronCreated()) when: patronRepo.publish(placedOnHold(book)) then: patronShouldBeFoundInDatabaseWithOneBookOnHold(patronId) and: bookReactedToPlacedOnHoldEvent() and: dailySheetIsUpdated() } void bookReactedToPlacedOnHoldEvent() { pollingConditions.eventually { assert bookRepository.findBy(book.bookId).get() instanceof BookOnHold } } void dailySheetIsUpdated() { pollingConditions.eventually { assert countOfHoldsInDailySheet() == 1 } }Please note that the test looks exactly the same as previous one, but now we utilized Groovy's PollingConditions to perform asynchronous functionality tests
Sample implementation of event bus is following:
@AllArgsConstructor public class StoreAndForwardDomainEventPublisher implements DomainEvents { private final JustForwardDomainEventPublisher justForwardDomainEventPublish
Related Skills
diffs
339.5kUse the diffs tool to produce real, shareable diffs (viewer URL, file artifact, or both) instead of manual edit summaries.
clearshot
Structured screenshot analysis for UI implementation and critique. Analyzes every UI screenshot with a 5×5 spatial grid, full element inventory, and design system extraction — facts and taste together, every time. Escalates to full implementation blueprint when building. Trigger on any digital interface image file (png, jpg, gif, webp — websites, apps, dashboards, mockups, wireframes) or commands like 'analyse this screenshot,' 'rebuild this,' 'match this design,' 'clone this.' Skip for non-UI images (photos, memes, charts) unless the user explicitly wants to build a UI from them. Does NOT trigger on HTML source code, CSS, SVGs, or any code pasted as text.
openpencil
1.8kThe world's first open-source AI-native vector design tool and the first to feature concurrent Agent Teams. Design-as-Code. Turn prompts into UI directly on the live canvas. A modern alternative to Pencil.
ui-ux-pro-max-skill
53.5kAn AI SKILL that provide design intelligence for building professional UI/UX multiple platforms
