MCPcopy Index your code
hub / github.com/ddd-by-examples/library

github.com/ddd-by-examples/library @main

Chat with this repo
repository ↗ · DeepWiki ↗ · + Follow
396 symbols 1,213 edges 97 files 1 documented · 0% updated 3y ago★ 5,81418 open issues
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

CircleCI Code Coverage

Table of contents

  1. About
  2. Domain description
  3. 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
  4. How to contribute
  5. 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:
Event Storming Domain description
The EventStorming session led us to numerous discoveries, modeled with the sticky notes:
Event Storming Big Picture
During the session we discovered following definitions:
Event Storming Definitions

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

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

Please follow the links below to get more details on each of the mentioned steps: - Big Picture EventStorming - Example Mapping - Design Level EventStorming

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.

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 ```groovy 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: ```java @AllArgsConstructor public class JustForwardDomainEventPublisher implements DomainEvents {

    private final ApplicationEventPublisher applicationEventPublisher;

    @Override
    public void publish(DomainEvent event) {
        applicationEventPublisher.publishEvent(event);
    }
}
```
  • Eventual consistency ```groovy 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:

    ```java @AllArgsConstructor public class StoreAndForwardDomainEventPublisher implements DomainEvents {

    private final JustForwardDomainEventPublisher justForwardDomainEventPublisher;
    private final EventsStorage eventsStorage;
    
    @Override
    public void publish(DomainEvent event) {
        eventsStorage.save(event);
    }
    
    @Scheduled(fixedRate = 3000L)
    @Transactional
    public void publishAllPeriodically() {
        List<DomainEvent> domainEvents = eventsStorage.toPublish();
        domainEvents.forEach(justForwardDomainEventPublisher::publish);
        eventsStorage.published(domainEvents);
    }
    

    } ```

To clarify, we should always aim for aggregates that can handle a business operation atomically (transactionally if you like), so each aggregate should be as independent and decoupled from other aggregates as possible. Thus, eventual consistency is promoted. As we already mentioned, it comes with some tradeoffs, so from the pragmatic point of view immediate consistency is also a choice. You might ask yourself a question now: What if I don't have any events yet?. Well, a pragmatic approach would be to encapsulate the communication between aggregates in a Service-like class, where you could call proper aggregates line by line explicitly.

Events

Talking about inter-aggregate communication, we must remember that events reduce coupling, but don't remove it completely. Thus, it is very vital to share(publish) only those events, that are necessary for other aggregates to exist and function. Otherwise there is a threat that the level of coupling will increase introducing feature envy, because other aggregates might start using those events to perform actions they are not supposed to perform. A solution to this problem could be the distinction of domain events and integration events, which will be described here soon.

Events in Repositories

Repositories are one of the most popular design pattern. They abstract our domain model from data layer. In other words, they deal with state. That said, a common use-case is when we pass a new state to our repository, so that it gets persisted. It may look like so:

public class BusinessService {

    private final PatronRepository patronRepository;

    void businessMethod(PatronId patronId) {
        Patron patron = patronRepository.findById(patronId);
        //do sth
        patronRepository.save(patron);
    }
}

Conceptually, between 1st and 3rd line of that business method we change state of our Patron from A to B. This change might be calculated by dirty checking or we might just override entire Patron state in the database. Third option is Let's make implicit explicit and actually call this state change A->B an event. After all, event-driven architecture is all about promoting state changes as domain events.

Thanks to this our domain model may become immutable and just return events as results of invoking a command like so:

public BookPlacedOnHold placeOnHold(AvailableBook book) {
      ...
}

And our repository might operate directly on events like so:

public interface PatronRepository {
     void save(PatronEvent event) {
}

ArchUnit

One of the main components of a successful project is technical leadership that lets the team go in the right direction. Nevertheless, there are tools that can support teams in keeping the code clean and protect the architecture, so that the project won't become a Big Ball of Mud, and thus will be pleasant to develop and to maintain. The first option, the one we proposed, is ArchUnit - a Java architecture test tool. ArchUnit lets you write unit tests of your architecture, so that it is always consistent with initial vision. Maven modules could be an alternative as well, but let's focus on the former.

In terms of hexagonal architecture, it is essential to ensure, that we do not mix different levels of abstraction (hexagon levels): ```java @ArchTest public static final ArchRule model_should_not_depend_on_infrastructure = noClasses() .

Extension points exported contracts — how you extend this code

PatronEvent (Interface)
(no doc) [12 implementers]
src/main/java/io/pillopl/library/lending/patron/model/PatronEvent.java
DomainEvents (Interface)
(no doc) [9 implementers]
src/main/java/io/pillopl/library/commons/events/DomainEvents.java
BookRepository (Interface)
(no doc) [4 implementers]
src/main/java/io/pillopl/library/lending/book/model/BookRepository.java
Book (Interface)
(no doc) [3 implementers]
src/main/java/io/pillopl/library/lending/book/model/Book.java
PatronProfiles (Interface)
(no doc) [2 implementers]
src/main/java/io/pillopl/library/lending/patronprofile/model/PatronProfiles.java

Core symbols most depended-on inside this repo

getBookId
called by 78
src/main/java/io/pillopl/library/lending/book/model/BookOnHold.java
getPatronId
called by 73
src/main/java/io/pillopl/library/lending/patron/model/PatronEvent.java
of
called by 46
src/main/java/io/pillopl/library/lending/patron/model/NumberOfDays.java
map
called by 42
src/main/java/io/pillopl/library/lending/patron/infrastructure/PatronsDatabaseRepository.java
now
called by 28
src/main/java/io/pillopl/library/lending/patron/model/PatronEvent.java
instanceOf
called by 24
src/main/java/io/pillopl/library/catalogue/BookInstance.java
fetchFor
called by 14
src/main/java/io/pillopl/library/lending/patronprofile/model/PatronProfiles.java
is
called by 14
src/main/java/io/pillopl/library/lending/patron/infrastructure/HoldDatabaseEntity.java

Shape

Method 273
Class 105
Interface 13
Enum 5

Languages

Java100%

Modules by API surface

src/main/java/io/pillopl/library/lending/patron/model/PatronEvent.java30 symbols
src/test/groovy/io/pillopl/library/lending/patron/model/PatronFixture.java19 symbols
src/main/java/io/pillopl/library/lending/patronprofile/web/PatronProfileController.java17 symbols
src/integration-test/groovy/io/pillopl/library/lending/patronprofile/web/PatronProfileControllerIT.java14 symbols
src/main/java/io/pillopl/library/lending/dailysheet/infrastructure/SheetsReadModel.java12 symbols
src/test/groovy/io/pillopl/library/lending/book/model/BookFixture.java11 symbols
src/main/java/io/pillopl/library/lending/patron/infrastructure/PatronsDatabaseRepository.java11 symbols
src/main/java/io/pillopl/library/lending/book/infrastructure/BookDatabaseRepository.java10 symbols
src/main/java/io/pillopl/library/lending/book/application/PatronEventsHandler.java9 symbols
src/main/java/io/pillopl/library/lending/patron/model/Patron.java8 symbols
src/main/java/io/pillopl/library/lending/patron/infrastructure/PatronConfiguration.java8 symbols
src/main/java/io/pillopl/library/lending/patronprofile/web/WebConfiguration.java7 symbols

Dependencies from manifests, versioned

com.h2database:h21.4.197 · 1×
com.tngtech.archunit:archunit-junit40.9.3 · 1×
io.micrometer:micrometer-registry-prometheus
io.vavr:vavr0.9.2 · 1×
org.spockframework:spock-core1.2-groovy-2.5 · 1×
org.spockframework:spock-spring1.2-groovy-2.5 · 1×
org.springframework.boot:spring-boot-starter
org.springframework.boot:spring-boot-starter-actuator
org.springframework.boot:spring-boot-starter-hateoas
org.springframework.boot:spring-boot-starter-test

For agents

$ claude mcp add library \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact