Overall Architecture
The first thing developers wonder about when they start writing business logic is which class the code that reads and writes the database belongs in. Writing the table access directly at the entry point of an API does work, as far as working goes. Even so, the JavaEE development model divides everything from the entry point down to SQL execution into four layers, and fixes the direction of calls across those layers to a single direction.
The Layer Structure
Presentation layer … The entry point for the REST API. Input validation and DTO conversion
↓
Application layer … Executing use cases. The entry point for batch jobs
↓
Domain layer … Business rules. Domain models and interface declarations
↓
Infrastructure layer … Persistence and external integration. Implementations of the domain interfaces
↓
Database
The arrows show the direction of calls. The presentation layer calls a use case in the application layer, and the use case calls a service in the domain layer. An endpoint that manipulates a domain model directly, or that calls a DAO itself, skips over layers and is not permitted.
| Layer | Responsibilities | Main classes |
|---|---|---|
| Presentation layer | The external interface, request and response conversion, input validation, authentication and authorization | Endpoint, request DTO, response DTO, Validator |
| Application layer | Executing use cases and coordinating domain services | UseCase, Job |
| Domain layer | Business rules, domain models, and interface declarations | Model, service interface, repository interface, exception |
| Infrastructure layer | Persisting data, integrating with external systems, implementing the domain interfaces | Entity, DAO, standard implementation classes |
From here on, the examples use the foo_order table, which holds order information, and the Order family of classes lined up around it.
How Dependencies Are Turned Inward
In the order of calls, the infrastructure layer comes after the domain layer. The direction of dependency runs the other way.
What lives in the domain layer is only the interfaces, such as OrderRepository and OrderService.
Implementation classes like StandardOrderRepository live in the infrastructure layer and implement the interfaces declared in the domain layer.
When a service calls a repository, what it references is the domain-layer interface; the service has no idea that im_mirage is running behind it.
Drawn as concentric circles, the domain layer that carries the business rules sits at the center, and the presentation and infrastructure layers both sit outside it. Dependencies pointing inward means that the outer layers know the center, while the center knows nothing of the outer layers.
Whether the direction of dependency is being respected shows up in the import statements.
If a class in the domain layer imports jp.co.intra_mart.mirage.ext.session.SessionTemplate or jp.co.intra_mart.mirage.ext.dao.DAOFactory, the mechanics of database access have made their way into the domain layer.
Package Structure
Packages are split by layer, with a package for each role underneath.
jp.co.example.foo
├── presentation
│ ├── endpoint // REST API endpoints
│ ├── request // Request DTOs
│ ├── response // Response DTOs
│ └── validator // Input validation
├── application
│ ├── usecase // Use cases
│ ├── job // Batch jobs
│ └── exception // Application exceptions
├── domain
│ ├── model // Domain models
│ ├── service // Service interfaces and factories
│ ├── repository // Repository interfaces and factories
│ └── exception // Domain exceptions
└── infrastructure
├── entity // Entities (database mapping)
├── dao // DAOs (calls into im_mirage)
├── model // Models used only inside the infrastructure layer
├── repository // Repository implementations and default factories
└── service // Service implementations and default factories
Naming Classes
The suffix on a class name tells you which layer it belongs to.
| Layer | Class | Naming |
|---|---|---|
| Presentation layer | Endpoint | {Resource}Endpoint |
| Request DTO | {Operation}Request | |
| Response DTO | {Operation}Response | |
| Validator | {Operation}Validator | |
| Application layer | Use case | {Operation}UseCase |
| Batch job | {JobName}Job | |
| Domain layer | Domain model | The domain name itself (Order) |
| Service interface | {DomainName}Service | |
| Repository interface | {DomainName}Repository | |
| Factory | {DomainName}ServiceFactory / {DomainName}RepositoryFactory | |
| Infrastructure layer | Entity | {DomainName}Entity |
| DAO | {DomainName}DAO | |
| Implementation | Standard{DomainName}Service / Standard{DomainName}Repository | |
| Default factory | Standard{DomainName}ServiceFactory / Standard{DomainName}RepositoryFactory |
Interface names do not take an I prefix.
Implementation classes do not take a mechanical Impl either; they take the Standard prefix, which marks them as the default implementation.
Method names follow the same discipline.
Lookups are findBy{Criteria}, findAll{EntityName}s, and findLatest{EntityName}, business operations are process{Operation} and calculate{Metric}, checks are validate{Rule} and is{State}, and conversions are convertTo{Type} and transformTo{Format}.
The entry point of a use case and of a job is execute in both cases.
Why Entities and Domain Models Are Separate
The same "order" can appear as a class in two places, under infrastructure.entity and under domain.model.
It looks like duplication, but the two belong to different layers and satisfy different constraints.
| Aspect | Entity | Domain model |
|---|---|---|
| Layer | Infrastructure layer | Domain layer |
| Purpose | One-to-one mapping to a database table | Expressing business logic |
| Fields | Public fields (a requirement of the mapping) | Private fields with getters |
| Mutability | Mutable (values are set during mapping) | Immutable preferred |
| Validation | None (relies on database constraints) | Implements business rules |
| Conversion | Converted in the repository layer | Converted in the repository layer |
An entity has public fields not because that reads better. im_mirage, which handles the object-relational mapping (ORM), is designed to map values directly into public fields, and that constraint is a matter for the infrastructure layer. Writing business rules into an entity lets the needs of the database access foundation bleed into the way the domain is expressed. Implement business rules in the domain model, and confine the conversion between the two to the repository implementation class.
For the same reason, an entity cannot be returned as an API response. Column names and audit fields become visible to the client, and any change to the table definition breaks the compatibility of the API. What goes out is a response DTO.
The detailed conventions for entities are covered in Creating Entities and DAOs.
The Exception Hierarchy
Every time an exception crosses a layer, it is translated into the vocabulary of that layer.
| Layer | Exception class | Kind | Purpose |
|---|---|---|---|
| Presentation layer | ValidationException | Unchecked | A malformed request |
| Application layer | {AppName}Exception | Checked | A use case that failed |
| Domain layer | {DomainName}ServiceException, RepositoryException | Checked | A business rule violation, or a failure to persist |
| Infrastructure layer and factories | {DomainName}RuntimeException | Unchecked | An implementation error, such as a factory that fails to load |
A repository in the infrastructure layer translates im_mirage's SQLRuntimeException into RepositoryException before handing it to the domain layer.
Pass the original exception as the cause on every translation.
Dropping the cause erases the originating layer from the stack trace.
The exception classes and the conventions for their messages are covered in Domain Layer Implementation Rules, and the rules for translating them in Infrastructure Layer Implementation Rules.
Gathering Construction in Factories
Instances of services and repositories are obtained from a factory rather than with new.
final OrderService orderService = OrderServiceFactory.getInstance().getOrderService();
Writing new StandardOrderService() pins the caller to the default implementation.
With a factory in between, the implementation can be swapped through ServiceLoaderUtil, and a mock can be injected in tests.
The factory is an abstract class and goes in the same domain-layer package as the interface.
StandardOrderServiceFactory, which returns the default implementation, goes in the infrastructure layer, and the abstract factory falls back to it only when no replacement is provided.
The point is to confine the tie to the default implementation to that single place.
How factories are implemented is covered in Domain Layer Implementation Rules.
Services and Repositories Establish the Transaction Boundary
Database access, whether it is an update or a read, runs inside SessionTemplate.execute(SessionCallback).
That boundary is established in both the domain service implementation and the repository implementation.
SessionTemplate can be nested, and an inner boundary joins the outer one.
A repository called from a service takes part in the service's transaction, so it is the service that decides the unit of commit.
The service is the only layer that knows which repositories are called and how many times, and therefore the only one that can combine updates spanning several repositories into a single unit.
Placing a boundary on the repository side as well means that even when a repository is called on its own, it never accesses the database outside a boundary.
Writing a SessionTemplate in an endpoint or a use case, on the other hand, spreads the dependency on im_mirage out to the outer layers and undermines the point of hiding the repository behind an interface.
How the boundary is established is covered in Infrastructure Layer Implementation Rules, and how it behaves when nested in Transaction Control.
Related Documentation
- Creating Entities and DAOs: Mapping with
@Table/@Column/@PrimaryKey, the supported column types, and the audit fields that are set automatically - 2WaySQL: Writing dynamic queries as SQL files
- Transaction Control:
SessionTemplateand nested transactions - Domain Layer Implementation Rules: Domain models, the repository and service interfaces, factories, and exception classes
- Infrastructure Layer Implementation Rules: Implementation classes, transaction boundaries, and exception translation
- Presentation Layer Implementation Rules: Endpoints, DTOs, and input validation
- Application Layer Implementation Rules: Use cases and batch jobs
- Anti-Patterns: Typical layer violations and how to fix them
- An Implementation Across Every Layer: The full set of classes from DDL to endpoint
The conventions for laying out the source tree are covered in Module Project Structure.
What is covered here is the implementation for the JavaEE development model.
Database access in the script development model (JSSP) uses the TenantDatabase and SharedDatabase APIs, and both the way SQL is written and the way it is called are different.
Take care not to carry an implementation from one development model over to the other.