docs: preserve upstream English README
This commit is contained in:
+820
@@ -0,0 +1,820 @@
|
||||
[](https://circleci.com/gh/ddd-by-examples/library)
|
||||
[](https://codecov.io/gh/ddd-by-examples/library)
|
||||
|
||||
# Table of contents
|
||||
|
||||
1. [About](#about)
|
||||
2. [Domain description](#domain-description)
|
||||
3. [General assumptions](#general-assumptions)
|
||||
3.1 [Process discovery](#process-discovery)
|
||||
3.2 [Project structure and architecture](#project-structure-and-architecture)
|
||||
3.3 [Aggregates](#aggregates)
|
||||
3.4 [Events](#events)
|
||||
3.4.1 [Events in Repositories](#events-in-repositories)
|
||||
3.5 [ArchUnit](#archunit)
|
||||
3.6 [Functional thinking](#functional-thinking)
|
||||
3.7 [No ORM](#no-orm)
|
||||
3.8 [Architecture-code gap](#architecture-code-gap)
|
||||
3.9 [Model-code gap](#model-code-gap)
|
||||
3.10 [Spring](#spring)
|
||||
3.11 [Tests](#tests)
|
||||
4. [How to contribute](#how-to-contribute)
|
||||
5. [References](#references)
|
||||
|
||||
## About
|
||||
|
||||
This is a project of a library, driven by real [business requirements](#domain-description).
|
||||
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:
|
||||
- [Big Picture EventStorming](./docs/big-picture.md)
|
||||
- [Example Mapping](docs/example-mapping.md)
|
||||
- [Design Level EventStorming](docs/design-level.md)
|
||||
|
||||
### 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](#spring).
|
||||
|
||||
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:
|
||||
|
||||
```java
|
||||
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:
|
||||
|
||||
```java
|
||||
public BookPlacedOnHold placeOnHold(AvailableBook book) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
And our repository might operate directly on events like so:
|
||||
|
||||
```java
|
||||
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](https://www.archunit.org/) - 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()
|
||||
.that()
|
||||
.resideInAPackage("..model..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAPackage("..infrastructure..");
|
||||
```
|
||||
and that frameworks do not affect the domain model
|
||||
```java
|
||||
@ArchTest
|
||||
public static final ArchRule model_should_not_depend_on_spring =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAPackage("..io.pillopl.library.lending..model..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAPackage("org.springframework..");
|
||||
```
|
||||
|
||||
### Functional thinking
|
||||
When you look at the code you might find a scent of functional programming. Although we do not follow
|
||||
a _clean_ FP, we try to think of business processes as pipelines or workflows, utilizing functional style through
|
||||
following concepts.
|
||||
|
||||
_Please note that this is not a reference project for FP._
|
||||
|
||||
#### Immutable objects
|
||||
Each class that represents a business concept is immutable, thanks to which we:
|
||||
* provide full encapsulation and objects' states protection,
|
||||
* secure objects for multithreaded access,
|
||||
* control all side effects much clearer.
|
||||
|
||||
#### Pure functions
|
||||
We model domain operations, discovered in Design Level Event Storming, as pure functions, and declare them in
|
||||
both domain and application layers in the form of Java's functional interfaces. Their implementations are placed
|
||||
in infrastructure layer as ordinary methods with side effects. Thanks to this approach we can follow the abstraction
|
||||
of ubiquitous language explicitly, and keep this abstraction implementation-agnostic. As an example, you could have
|
||||
a look at `FindAvailableBook` interface and its implementation:
|
||||
|
||||
```java
|
||||
@FunctionalInterface
|
||||
public interface FindAvailableBook {
|
||||
|
||||
Option<AvailableBook> findAvailableBookBy(BookId bookId);
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@AllArgsConstructor
|
||||
class BookDatabaseRepository implements FindAvailableBook {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public Option<AvailableBook> findAvailableBookBy(BookId bookId) {
|
||||
return Match(findBy(bookId)).of(
|
||||
Case($Some($(instanceOf(AvailableBook.class))), Option::of),
|
||||
Case($(), Option::none)
|
||||
);
|
||||
}
|
||||
|
||||
Option<Book> findBy(BookId bookId) {
|
||||
return findBookById(bookId)
|
||||
.map(BookDatabaseEntity::toDomainModel);
|
||||
}
|
||||
|
||||
private Option<BookDatabaseEntity> findBookById(BookId bookId) {
|
||||
return Try
|
||||
.ofSupplier(() -> of(jdbcTemplate.queryForObject("SELECT b.* FROM book_database_entity b WHERE b.book_id = ?",
|
||||
new BeanPropertyRowMapper<>(BookDatabaseEntity.class), bookId.getBookId())))
|
||||
.getOrElse(none());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Type system
|
||||
_Type system - like_ modelling - we modelled each domain object's state discovered during EventStorming as separate
|
||||
classes: `AvailableBook`, `BookOnHold`, `CheckedOutBook`. With this approach we provide much clearer abstraction than
|
||||
having a single `Book` class with an enum-based state management. Moving the logic to these specific classes brings
|
||||
Single Responsibility Principle to a different level. Moreover, instead of checking invariants in every business method
|
||||
we leave the role to the compiler. As an example, please consider following scenario: _you can place on hold only a book
|
||||
that is currently available_. We could have done it in a following way:
|
||||
```java
|
||||
public Either<BookHoldFailed, BookPlacedOnHoldEvents> placeOnHold(Book book) {
|
||||
if (book.status == AVAILABLE) {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
but we use the _type system_ and declare method of following signature
|
||||
```java
|
||||
public Either<BookHoldFailed, BookPlacedOnHoldEvents> placeOnHold(AvailableBook book) {
|
||||
...
|
||||
}
|
||||
```
|
||||
The more errors we discover at compile time the better.
|
||||
|
||||
Yet another advantage of applying such type system is that we can represent business flows and state transitions
|
||||
with functions much easier. As an example, following functions:
|
||||
```
|
||||
placeOnHold: AvailableBook -> BookHoldFailed | BookPlacedOnHold
|
||||
cancelHold: BookOnHold -> BookHoldCancelingFailed | BookHoldCanceled
|
||||
```
|
||||
are much more concise and descriptive than these:
|
||||
```
|
||||
placeOnHold: Book -> BookHoldFailed | BookPlacedOnHold
|
||||
cancelHold: Book -> BookHoldCancelingFailed | BookHoldCanceled
|
||||
```
|
||||
as here we have a lot of constraints hidden within function implementations.
|
||||
|
||||
Moreover if you think of your domain as a set of operations (functions) that are being executed on business objects
|
||||
(aggregates) you don't think of any execution model (like async processing). It is fine, because you don't have to.
|
||||
Domain functions are free from I/O operations, async, and other side-effects-prone things, which are put into the
|
||||
infrastructure layer. Thanks to this, we can easily test them without mocking mentioned parts.
|
||||
|
||||
#### Monads
|
||||
Business methods might have different results. One might return a value or a `null`, throw an exception when something
|
||||
unexpected happens or just return different objects under different circumstances. All those situations are typical
|
||||
to object-oriented languages like Java, but do not fit into functional style. We are dealing with this issues
|
||||
with monads (monadic containers provided by [Vavr](https://www.vavr.io)):
|
||||
* When a method returns optional value, we use the `Option` monad:
|
||||
|
||||
```java
|
||||
Option<Book> findBy(BookId bookId) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
* When a method might return one of two possible values, we use the `Either` monad:
|
||||
|
||||
```java
|
||||
Either<BookHoldFailed, BookPlacedOnHoldEvents> placeOnHold(AvailableBook book) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
* When an exception might occur, we use `Try` monad:
|
||||
|
||||
```java
|
||||
Try<Result> placeOnHold(@NonNull PlaceOnHoldCommand command) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Thanks to this, we can follow the functional programming style, but we also enrich our domain language and
|
||||
make our code much more readable for the clients.
|
||||
|
||||
#### Pattern Matching
|
||||
Depending on a type of a given book object we often need to perform different actions. Series of if/else or switch/case statements
|
||||
could be a choice, but it is the pattern matching that provides the most conciseness and flexibility. With the code
|
||||
like below we can check numerous patterns against objects and access their constituents, so our code has a minimal dose
|
||||
of language-construct noise:
|
||||
```java
|
||||
private Book handleBookPlacedOnHold(Book book, BookPlacedOnHold bookPlacedOnHold) {
|
||||
return API.Match(book).of(
|
||||
Case($(instanceOf(AvailableBook.class)), availableBook -> availableBook.handle(bookPlacedOnHold)),
|
||||
Case($(instanceOf(BookOnHold.class)), bookOnHold -> raiseDuplicateHoldFoundEvent(bookOnHold, bookPlacedOnHold)),
|
||||
Case($(), () -> book)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### (No) ORM
|
||||
If you run `mvn dependency:tree` you won't find any JPA implementation. Although we think that ORM solutions (like Hibernate)
|
||||
are very powerful and useful, we decided not to use them, as we wouldn't utilize their features. What features are
|
||||
talking about? Lazy loading, caching, dirty checking. Why don't we need them? We want to have more control
|
||||
over SQL queries and minimize the object-relational impedance mismatch ourselves. Moreover, thanks to relatively
|
||||
small aggregates, containing as little data as it is required to protect the invariants, we don't need the
|
||||
lazy loading mechanism either.
|
||||
With Hexagonal Architecture we have the ability to separate domain and persistence models and test them
|
||||
independently. Moreover, we can also introduce different persistence strategies for different aggregates.
|
||||
In this project, we utilize both plain SQL queries and `JdbcTemplate` and use new and very promising
|
||||
project called Spring Data JDBC, that is free from the JPA-related overhead mentioned before.
|
||||
Please find below an example of a repository:
|
||||
|
||||
```java
|
||||
interface PatronEntityRepository extends CrudRepository<PatronDatabaseEntity, Long> {
|
||||
|
||||
@Query("SELECT p.* FROM patron_database_entity p where p.patron_id = :patronId")
|
||||
PatronDatabaseEntity findByPatronId(@Param("patronId") UUID patronId);
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
At the same time we propose other way of persisting aggregates, with plain SQL queries and `JdbcTemplate`:
|
||||
|
||||
```java
|
||||
@AllArgsConstructor
|
||||
class BookDatabaseRepository implements BookRepository, FindAvailableBook, FindBookOnHold {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public Option<Book> findBy(BookId bookId) {
|
||||
return findBookById(bookId)
|
||||
.map(BookDatabaseEntity::toDomainModel);
|
||||
}
|
||||
|
||||
private Option<BookDatabaseEntity> findBookById(BookId bookId) {
|
||||
return Try
|
||||
.ofSupplier(() -> of(jdbcTemplate.queryForObject("SELECT b.* FROM book_database_entity b WHERE b.book_id = ?",
|
||||
new BeanPropertyRowMapper<>(BookDatabaseEntity.class), bookId.getBookId())))
|
||||
.getOrElse(none());
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
_Please note that despite having the ability to choose different persistence implementations for aggregates
|
||||
it is recommended to stick to one option within the app/team_
|
||||
|
||||
### Architecture-code gap
|
||||
We put a lot of attention to keep the consistency between the overall architecture (including diagrams)
|
||||
and the code structure. Having identified bounded contexts we could organize them in modules (packages, to
|
||||
be more specific). Thanks to this we gain the famous microservices' autonomy, while having a monolithic
|
||||
application. Each package has well defined public API, encapsulating all implementation details by using
|
||||
package-protected or private scopes.
|
||||
|
||||
Just by looking at the package structure:
|
||||
|
||||
```
|
||||
└── library
|
||||
├── catalogue
|
||||
├── commons
|
||||
│ ├── aggregates
|
||||
│ ├── commands
|
||||
│ └── events
|
||||
│ └── publisher
|
||||
└── lending
|
||||
├── book
|
||||
│ ├── application
|
||||
│ ├── infrastructure
|
||||
│ └── model
|
||||
├── dailysheet
|
||||
│ ├── infrastructure
|
||||
│ └── model
|
||||
├── librarybranch
|
||||
│ └── model
|
||||
├── patron
|
||||
│ ├── application
|
||||
│ ├── infrastructure
|
||||
│ └── model
|
||||
└── patronprofile
|
||||
├── infrastructure
|
||||
├── model
|
||||
└── web
|
||||
```
|
||||
you can see that the architecture is screaming that it has two bounded contexts: **catalogue**
|
||||
and **lending**. Moreover, the **lending context** is built around five business objects: **book**,
|
||||
**dailysheet**, **librarybranch**, **patron**, and **patronprofile**, while **catalogue** has no subpackages,
|
||||
which suggests that it might be a CRUD with no complex logic inside. Please find the architecture diagram
|
||||
below.
|
||||
|
||||

|
||||
|
||||
Yet another advantage of this approach comparing to packaging by layer for example is that in order to
|
||||
deliver a functionality you would usually need to do it in one package only, which is the aforementioned
|
||||
autonomy. This autonomy, then, could be transferred to the level of application as soon as we split our
|
||||
_context-packages_ into separate microservices. Following this considerations, autonomy can be given away
|
||||
to a product team that can take care of the whole business area end-to-end.
|
||||
|
||||
### Model-code gap
|
||||
In our project we do our best to reduce _model-code gap_ to bare minimum. It means we try to put equal attention
|
||||
to both the model and the code and keep them consistent. Below you will find some examples.
|
||||
|
||||
#### Placing on hold
|
||||

|
||||
|
||||
Starting with the easiest part, below you will find the model classes corresponding to depicted command and events:
|
||||
|
||||
```java
|
||||
@Value
|
||||
class PlaceOnHoldCommand {
|
||||
...
|
||||
}
|
||||
```
|
||||
```java
|
||||
@Value
|
||||
class BookPlacedOnHold implements PatronEvent {
|
||||
...
|
||||
}
|
||||
```
|
||||
```java
|
||||
@Value
|
||||
class MaximumNumberOfHoldsReached implements PatronEvent {
|
||||
...
|
||||
}
|
||||
```
|
||||
```java
|
||||
@Value
|
||||
class BookHoldFailed implements PatronEvent {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
We know it might not look impressive now, but if you have a look at the implementation of an aggregate,
|
||||
you will see that the code reflects not only the aggregate name, but also the whole scenario of `PlaceOnHold`
|
||||
command handling. Let us uncover the details:
|
||||
|
||||
```java
|
||||
public class Patron {
|
||||
|
||||
public Either<BookHoldFailed, BookPlacedOnHoldEvents> placeOnHold(AvailableBook book) {
|
||||
return placeOnHold(book, HoldDuration.openEnded());
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The signature of `placeOnHold` method screams, that it is possible to place a book on hold only when it
|
||||
is available (more information about protecting invariants by compiler you will find in [Type system section](#type-system)).
|
||||
Moreover, if you try to place available book on hold it can **either** fail (`BookHoldFailed`) or produce some events -
|
||||
what events?
|
||||
|
||||
```java
|
||||
@Value
|
||||
class BookPlacedOnHoldEvents implements PatronEvent {
|
||||
@NonNull UUID eventId = UUID.randomUUID();
|
||||
@NonNull UUID patronId;
|
||||
@NonNull BookPlacedOnHold bookPlacedOnHold;
|
||||
@NonNull Option<MaximumNumberOfHoldsReached> maximumNumberOfHoldsReached;
|
||||
|
||||
@Override
|
||||
public Instant getWhen() {
|
||||
return bookPlacedOnHold.when;
|
||||
}
|
||||
|
||||
public static BookPlacedOnHoldEvents events(BookPlacedOnHold bookPlacedOnHold) {
|
||||
return new BookPlacedOnHoldEvents(bookPlacedOnHold.getPatronId(), bookPlacedOnHold, Option.none());
|
||||
}
|
||||
|
||||
public static BookPlacedOnHoldEvents events(BookPlacedOnHold bookPlacedOnHold, MaximumNumberOfHoldsReached maximumNumberOfHoldsReached) {
|
||||
return new BookPlacedOnHoldEvents(bookPlacedOnHold.patronId, bookPlacedOnHold, Option.of(maximumNumberOfHoldsReached));
|
||||
}
|
||||
|
||||
public List<DomainEvent> normalize() {
|
||||
return List.<DomainEvent>of(bookPlacedOnHold).appendAll(maximumNumberOfHoldsReached.toList());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`BookPlacedOnHoldEvents` is a container for `BookPlacedOnHold` event, and - if patron has 5 book placed on hold already -
|
||||
`MaximumNumberOfHoldsReached` (please mind the `Option` monad). You can see now how perfectly the code reflects
|
||||
the model.
|
||||
|
||||
It is not everything, though. In the picture above you can also see a big rectangular yellow card with rules (policies)
|
||||
that define the conditions that need to be fulfilled in order to get the given result. All those rules are implemented
|
||||
as functions **either** allowing or rejecting the hold:
|
||||
|
||||

|
||||
```java
|
||||
PlacingOnHoldPolicy onlyResearcherPatronsCanHoldRestrictedBooksPolicy = (AvailableBook toHold, Patron patron, HoldDuration holdDuration) -> {
|
||||
if (toHold.isRestricted() && patron.isRegular()) {
|
||||
return left(Rejection.withReason("Regular patrons cannot hold restricted books"));
|
||||
}
|
||||
return right(new Allowance());
|
||||
};
|
||||
```
|
||||
|
||||

|
||||
|
||||
```java
|
||||
PlacingOnHoldPolicy overdueCheckoutsRejectionPolicy = (AvailableBook toHold, Patron patron, HoldDuration holdDuration) -> {
|
||||
if (patron.overdueCheckoutsAt(toHold.getLibraryBranch()) >= OverdueCheckouts.MAX_COUNT_OF_OVERDUE_RESOURCES) {
|
||||
return left(Rejection.withReason("cannot place on hold when there are overdue checkouts"));
|
||||
}
|
||||
return right(new Allowance());
|
||||
};
|
||||
```
|
||||
|
||||

|
||||
|
||||
```java
|
||||
PlacingOnHoldPolicy regularPatronMaximumNumberOfHoldsPolicy = (AvailableBook toHold, Patron patron, HoldDuration holdDuration) -> {
|
||||
if (patron.isRegular() && patron.numberOfHolds() >= PatronHolds.MAX_NUMBER_OF_HOLDS) {
|
||||
return left(Rejection.withReason("patron cannot hold more books"));
|
||||
}
|
||||
return right(new Allowance());
|
||||
};
|
||||
```
|
||||
|
||||

|
||||
|
||||
```java
|
||||
PlacingOnHoldPolicy onlyResearcherPatronsCanPlaceOpenEndedHolds = (AvailableBook toHold, Patron patron, HoldDuration holdDuration) -> {
|
||||
if (patron.isRegular() && holdDuration.isOpenEnded()) {
|
||||
return left(Rejection.withReason("regular patron cannot place open ended holds"));
|
||||
}
|
||||
return right(new Allowance());
|
||||
};
|
||||
```
|
||||
|
||||
#### Spring
|
||||
Spring Framework seems to be the most popular Java framework ever used. Unfortunately it is also quite common
|
||||
to overuse its features in the business code. What you find in this project is that the domain packages
|
||||
are fully focused on modelling business problems, and are free from any DI, which makes it easy to
|
||||
unit-test it which is invaluable in terms of code reliability and maintainability. It does not mean,
|
||||
though, that we do not use Spring Framework - we do. Below you will find some details:
|
||||
- Each bounded context has its own independent application context. It means that we removed the runtime
|
||||
coupling, which is a step towards extracting modules (and microservices). How did we do that? Let's have
|
||||
a look:
|
||||
```java
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
public class LibraryApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
new SpringApplicationBuilder()
|
||||
.parent(LibraryApplication.class)
|
||||
.child(LendingConfig.class).web(WebApplicationType.SERVLET)
|
||||
.sibling(CatalogueConfiguration.class).web(WebApplicationType.NONE)
|
||||
.run(args);
|
||||
}
|
||||
}
|
||||
```
|
||||
- As you could see above, we also try not to use component scan wherever possible. Instead we utilize
|
||||
`@Configuration` classes where we define module specific beans in the infrastructure layer. Those
|
||||
configuration classes are explicitly declared in the main application class.
|
||||
|
||||
### Tests
|
||||
Tests are written in a BDD manner, expressing stories defined with Example Mapping.
|
||||
It means we utilize both TDD and Domain Language discovered with Event Storming.
|
||||
|
||||
We also made an effort to show how to create a DSL, that enables to write
|
||||
tests as if they were sentences taken from the domain descriptions. Please
|
||||
find an example below:
|
||||
|
||||
```groovy
|
||||
def 'should make book available when hold canceled'() {
|
||||
given:
|
||||
BookDSL bookOnHold = aCirculatingBook() with anyBookId() locatedIn anyBranch() placedOnHoldBy anyPatron()
|
||||
and:
|
||||
PatronEvent.BookHoldCanceled bookHoldCanceledEvent = the bookOnHold isCancelledBy anyPatron()
|
||||
|
||||
when:
|
||||
AvailableBook availableBook = the bookOnHold reactsTo bookHoldCanceledEvent
|
||||
then:
|
||||
availableBook.bookId == bookOnHold.bookId
|
||||
availableBook.libraryBranch == bookOnHold.libraryBranchId
|
||||
availableBook.version == bookOnHold.version
|
||||
}
|
||||
```
|
||||
_Please also note the **when** block, where we manifest the fact that books react to
|
||||
cancellation event_
|
||||
|
||||
## How to contribute
|
||||
|
||||
The project is still under construction, so if you like it enough to collaborate, just let us
|
||||
know or simply create a Pull Request.
|
||||
|
||||
|
||||
## How to Build
|
||||
|
||||
### Requirements
|
||||
|
||||
* Java 11
|
||||
* Maven
|
||||
|
||||
### Quickstart
|
||||
|
||||
You can run the library app by simply typing the following:
|
||||
|
||||
```console
|
||||
$ mvn spring-boot:run
|
||||
...
|
||||
...
|
||||
2019-04-03 15:55:39.162 INFO 18957 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoint(s) beneath base path '/actuator'
|
||||
2019-04-03 15:55:39.425 INFO 18957 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
|
||||
2019-04-03 15:55:39.428 INFO 18957 --- [ main] io.pillopl.library.LibraryApplication : Started LibraryApplication in 5.999 seconds (JVM running for 23.018)
|
||||
|
||||
```
|
||||
|
||||
### Build a Jar package
|
||||
|
||||
You can build a jar with maven like so:
|
||||
|
||||
```console
|
||||
$ mvn clean package
|
||||
...
|
||||
...
|
||||
[INFO] Building jar: /home/pczarkowski/development/spring/library/target/library-0.0.1-SNAPSHOT.jar
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
### Build with Docker
|
||||
|
||||
If you've already built the jar file you can run:
|
||||
|
||||
```console
|
||||
docker build -t spring/library .
|
||||
```
|
||||
|
||||
Otherwise you can build the jar file using the multistage dockerfile:
|
||||
|
||||
```console
|
||||
docker build -t spring/library -f Dockerfile.build .
|
||||
```
|
||||
|
||||
Either way once built you can run it like so:
|
||||
|
||||
```console
|
||||
$ docker run -ti --rm --name spring-library -p 8080:8080 spring/library
|
||||
```
|
||||
|
||||
### Production ready metrics and visualization
|
||||
To run the application as well as Prometheus and Grafana dashboard for visualizing metrics you can run all services:
|
||||
|
||||
```console
|
||||
$ docker-compose up
|
||||
```
|
||||
|
||||
If everything goes well, you can access the following services at given location:
|
||||
* http://localhost:8080/actuator/prometheus - published Micrometer metrics
|
||||
* http://localhost:9090 - Prometheus dashboard
|
||||
* http://localhost:3000 - Grafana dashboard
|
||||
|
||||
In order to see some metrics, you must create a dashboard. Go to `Create` -> `Import` and select attached `jvm-micrometer_rev8.json`. File has been pulled from
|
||||
`https://grafana.com/grafana/dashboards/4701`.
|
||||
|
||||
Please note application will be run with `local` Spring profile to setup some initial data.
|
||||
|
||||
## References
|
||||
|
||||
1. [Introducing EventStorming](https://leanpub.com/introducing_eventstorming) by Alberto Brandolini
|
||||
2. [Domain Modelling Made Functional](https://pragprog.com/book/swdddf/domain-modeling-made-functional) by Scott Wlaschin
|
||||
3. [Software Architecture for Developers](https://softwarearchitecturefordevelopers.com) by Simon Brown
|
||||
4. [Clean Architecture](https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164) by Robert C. Martin
|
||||
5. [Domain-Driven Design: Tackling Complexity in the Heart of Software](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215) by Eric Evans
|
||||
Reference in New Issue
Block a user