- How to design and implement an application core
- How to publish Domain Events within the application core
- How to implement a Repository
- Changes to the main method
- Understand tutorial
HelloJexxaandTimeServicebecause we explain only new aspects - 60 minutes
- JDK 25 (or higher) installed
- Maven 3.6 (or higher) installed
- curl to trigger the application
- Optional: A postgres DB
This application core should provide the following super simplified functionality:
-
Manage available books in the store which means to add, sell, and query books
-
All books should be identified by their ISBN13
-
For each book we store the number of available copies in stock
-
Publish
DomainEventBookSoldOutif last copy of a book is sold -
A service which gets the latest books from our reference library. For this tutorial, it is sufficient that:
- Service provides a hardcoded list
- Service is triggered when starting the application
General note: There are several books, courses, tutorials available describing how to implement an application core using the patterns of DDD. The approach used in this tutorial should not be considered as reference. It serves only for demonstration purpose how to realize your decisions with Jexxa.
First, we map the functionality of the application to DDD patterns
Show mapping to DDD patterns
-
Domain:Book: Is anAggregatebecause it has a life-cycle that changes over time.BookRepository: Is aRepositorythat managesBookinstances.ISBN13: Is aValueObjectthat is immutable and identifies a bookBookSoldOut: Is aDomainEventthat informs us if a book is no longer in stockBookNotInStockException: Is aBusinessExceptionin case we try to sell a book that is currently not available
-
DomainService:DomainEventPublisher: Is aDomainServicethat registers for allDomainEventsand forwards them to an external message bus.ReferenceLibrary: Is aDomainServicethat provides primary data for our bookstore and returns the latest books.IntegrationEventSender: Is anInfrastructureServicethat sendsDomainEventsto a message bus.
-
ApplicationService:BookStoreService:Is anApplicationServicethat provides typical use cases such as selling a book.
Based on the mapping to DDD patterns, we derive the following package structure which is quite common in the DDD community:
Show package structure
-
applicationservice -
domainservice -
domain<use case 1>...<use case n>
-
infrastructuredrivenadapterdrivingadapter
Please note that a package for a specific use case includes all required domain classes.
As you can see in the examples, these are typically the corresponding of type Aggregate,ValueObject,
DomainEvent, BusinessException, and Repository.
The reason for this is
that you should apply the Common Closure Principle
so that changing classes within such a package is a change in the use case.
In addition, it should not affect any other use-cases.
Structuring your domain-package this way provides the following benefits:
- Use cases are represented explicitly which allows a clear view into the application
- Use cases remain strictly separated which simplifies adding, removing or changing use cases
- When designing your application, this approach supports discussing what kind of functionality belongs to a use case or not
As soon as your domain logic and thus the number of use cases grows, it will happen that ValueObjects will be used by multiple use cases. This is quite normal. The challenge is to group these ValueObjects and find a domain specific main term which is not common or something like this. As soon as you have this name, create a corresponding package within domain.
-
ValueObjectandDomainEvent: Are implemented using Java records due to the following reasons:- They are immutable and compared based on their internal values.
- They must not have set-methods. So all fields are final.
- They must provide a valid implementation of equals() and hashcode().
- They must not include any business logic, but they can validate their input data.
- Using records ensures that the canonical constructor is called, even if they are deserialized. So you can validate given values in constructor without considering serialization methods.
-
Aggregate: Is identified by a uniqueAggregateIDwhich is aValueObjectBookuses anISBN13object
-
Repositroywhen defining any interface within the application core ensure that you use the domain language for all methods. Resist the temptation to use the language of the used technology stack that you use to implement this interface.
As you can see in the source code, all classes are annotated with the pattern language of DDD. This is not required but strongly recommended. The explanation for this can be found in tutorial pattern language.
When sending DomainEvents, we should distinguish between two separate scenarios.
DomainEvent: Used to inform the application core that something happened which is then typically handled by anApplicationServiceor aDomainService.IntegrationEvent: Used to inform other bounded contexts that something important happened. These events are typically forwarded by anInfrastructureService.
Within the DDD community, there are essentially three different approaches on how to implement sending DomainEvents. An overview and discussion of these approaches can be found here.
Jexxa itself supports all of these approaches. In these tutorials, the approach with static methods is used because it is also used in the book Implementing Domain-Driven Design and described in great detail. It therefore forms an exceptional basis for the initial implementation of a DDD application, from which teams can then work out for their own approach.
The implementation of interface IntegrationEventSender is quite similar to message sender in tutorial TimeService.
So we don't discuss it here.
To simplify the implementation of a Repository Jexxa provides strategies that can be used. For example, the IRepository
interface provides typical CRUD operations and is especially designed to handle an Aggregate as you can see below:
@SuppressWarnings("unused")
public class BookRepositoryImpl implements BookRepository
{
private final IRepository<Book, ISBN13> repository;
public BookRepositoryImpl(Properties properties)
{
// Request a strategy to implement our repository
this.repository = getRepository(Book.class, Book::getISBN13, properties);
}
@Override
public void add(Book book) { repository.add(book); }
@Override
public void update(Book book) { repository.update(book); }
public void removeFromStock(ISBN13 isbn13) { bookRepository.remove(isbn13); }
@Override
public boolean isRegistered(ISBN13 isbn13) { return search(isbn13).isPresent(); }
@Override
public Book get(ISBN13 isbn13) { return repository.get(isbn13).orElseThrow(); }
@Override
public Optional<Book> search(ISBN13 isbn13) { return repository.get(isbn13); }
@Override
public List<Book> getAll() { return repository.get(); }
}Important: As you can see, the implementation of a repository and a message sender is straight forward. So it is a good starting point for junior developers. See here how you can use it to develop your junior developers.
Finally, we have to write our application. As you can see in the code below there is one difference compared to HelloJexxa and TimeService:
- Add a bootstrap service which is directly called to initialize domain-specific aspects.
public final class BookStore
{
static void main(String[] args)
{
var jexxaMain = new JexxaMain(BookStore.class);
jexxaMain
.bootstrap(ReferenceLibrary.class).and() // Bootstrap latest books via ReferenceLibrary
.bootstrap(IntegrationEventSender.class).with(sender -> subscribe(sender::publish)) // publish all DomainEvents as IntegrationEvents for other bounded contexts
.bind(RESTfulRPCAdapter.class).to(BookStoreService.class) // Provide REST access to BookStoreService
.bind(RESTfulRPCAdapter.class).to(jexxaMain.getBoundedContext()) // Provide REST access to BoundedContext
.run(); // Finally, run the application
}
}That's it.
mvn clean install
java -jar "-Dio.jexxa.config.import=./src/test/resources/jexxa-local.properties" \
./target/bookstore-jar-with-dependencies.jarYou will see the following (or similar) output
[main] INFO io.jexxa.utils.JexxaBanner - Config Information:
[main] INFO io.jexxa.utils.JexxaBanner - Jexxa Version : VersionInfo[version=5.0.0-SNAPSHOT, repository=scm:git:https://github.com/jexxa-projects/Jexxa.git/jexxa-core, projectName=Jexxa-Core, buildTimestamp=2022-06-16 15:39]
[main] INFO io.jexxa.utils.JexxaBanner - Context Version : VersionInfo[version=1.0.16-SNAPSHOT, repository=scm:git:https://github.com/jexxa-projects/JexxaTutorials.git/bookstore, projectName=BookStore, buildTimestamp=2022-06-16 18:07]
[main] INFO io.jexxa.utils.JexxaBanner - Used Driving Adapter : [RESTfulRPCAdapter]
[main] INFO io.jexxa.utils.JexxaBanner - Used Properties Files : [/jexxa-application.properties, ./src/test/resources/jexxa-local.properties]
[main] INFO io.jexxa.utils.JexxaBanner - Used Repository Strategie : [IMDBRepository]
[main] INFO io.jexxa.utils.JexxaBanner - Used Message Sender Strategie : [MessageLogger]
[main] INFO io.jexxa.utils.JexxaBanner -
[main] INFO io.jexxa.utils.JexxaBanner - Access Information:
[main] INFO io.jexxa.utils.JexxaBanner - Listening on: http://0.0.0.0:7505
[main] INFO io.jexxa.utils.JexxaBanner - OpenAPI available at: http://0.0.0.0:7505/swagger-docs
[main] INFO io.jexxa.core.JexxaMain - BoundedContext 'BookStore' successfully started in 1.280 seconds
The properties file jexxa-test.properties is configured to use a postgres DB. So we have to enter following command
mvn clean install
java -jar "-Dio.jexxa.config.import=./src/test/resources/jexxa-test.properties" ./target/bookstore-jar-with-dependencies.jarIn contrast to the above output, Jexxa will state that you use JDBC persistence strategy now:
[main] INFO io.jexxa.utils.JexxaBanner - Used Properties Files : [/jexxa-application.properties, ./src/test/resources/jexxa-test.properties]
[main] INFO io.jexxa.utils.JexxaBanner - Used Repository Strategie : [JDBCKeyValueRepository]
[main] INFO io.jexxa.utils.JexxaBanner - Used Message Sender Strategie : [JMSSender]Note: In case you want to use a difference database, you have to:
- Add the corresponding jdbc driver to pom.xml to dependencies-section.
- Adjust the section
#Settings for JDBCConnection to postgres DBin jexxa-test.properties.
Command:
curl -X GET http://localhost:7503/BookStoreService/getBooksResponse:
[
{"isbn13":"978-1-60309-322-4"},{"isbn13":"978-1-891830-85-3"},
{"isbn13":"978-1-60309-047-6"},{"isbn13":"978-1-60309-025-4"},
{"isbn13":"978-1-60309-016-2"},{"isbn13":"978-1-60309-265-4"}
]Command:
curl -X POST -H "Content-Type: application/json" -d '{isbn13:"978-1-891830-85-3"}' \
http://localhost:7503/BookStoreService/inStock Response:
falseCommand:
curl -X POST -H "Content-Type: application/json" -d "[{isbn13: "978-1-891830-85-3"}, 5]" \
http://localhost:7503/BookStoreService/addToStockResponse: No output
Command:
curl -X POST -H "Content-Type: application/json" -d '{isbn13:"978-1-891830-85-3"}' \
http://localhost:7503/BookStoreService/inStock Response:
true