This application is an example of a modular monolith architectural style.
A domain module has a hexagonal architecture:
-
domain
- the business logic -
web
- an inbound adapter that implements a REST API -
persistence
- an outbound adapter that implements database access
Domains collaborate in two different ways. The first is by one domain invoking another. For example:
@Transactional public Order createOrder(long customerId, Money orderTotal) { ... creditManagement.reserveCredit(customerId, order.getId(), orderTotal);
The second collaboration mechanism is the Observer pattern - one domain subscribes to events emitted by another domain. For example:
@Service public class NotificationServiceImpl implements CustomerDomainObserver { @PostConstruct public void registerCustomerDomainObserver() { customerDomainObservers.registerObserver(this); } @Override public void noteCustomerCreated(CustomerInfo customerInfo) {...}
Note: For simplicity, the observers are executed in the publisher’s transaction.
X is build-time coupled to Y if a change to Y requires X to be recompiled and, more importantly, retested. Since fast builds are essential, it’s important to minimize build-time coupling.
This example minimizes build-time coupling in a few different ways:
-
Domain module = multiple Gradle projects
-
Domain modules have a Domain API Gradle sub-project
-
Uses the Interface Segregation Principle (ISP) to define narrower APIs.
To reduce build-time coupling within a domain module, each domain module consists of multiple Gradle sub-projects.
Typically, the sub-projects correspond the elements of the hexagonal architecture - domain or adapter.
For example, the Customers
domain consists of multiple Gradle sub-projects including customers-domain
, customers-web
, and customers-persistence
.
An important benefit of this design is that it reduces build-time coupling.
A change, for example, to the web
adapter doesn’t require the domain
or persistence
adapter to be recompiled/retested.
The Customers
and Notifications
domain modules - specifically their domain logic - are used by the Orders
domain module.
In order to minimize build-time coupling, each of those domain modules has a Domain API Gradle sub-project, which defines that module’s API.
For example, the Customers
domain has a customers-api
Gradle sub-project, which contains its API classes.
A domain module’s clients only depend on its Domain API Gradle sub-project.
Moreover, its clients are tested using mocks of the API.
For example, the Orders
domain is tested using a mock of the Customers
domain’s API.
As a result, a domain module’s implementation can be changed without having to recompile/retest its clients.
To further reduce build-time coupling, the Customers
domain module uses the Interface Segregation Principle (ISP) to define narrower APIs.
It has three Domain Modules API Gradle sub-projects:
-
customers-api
- defines CRUD operations -
customers-api-credit-management
- defines operations for managing credit -
customers-api-observer
- for registering observers of theCustomers
domain
As a result, each client only build-time coupled to those APIs that it’s actually using.
This repository includes a series of Code Tours that explain the design of the application. You can use the code tours in either Visual Studio Code or Github Codespaces.
-
Install the Code Tour extension from the Visual Studio Code Marketplace
-
Use the
CodeTour: Start Tour
command from the command palette to start the tour
-
If necessary, install the Code Tour extension from the Visual Studio Code Marketplace.
-
Use the
CodeTour: Start Tour
command from the command palette to start the tour.