Skip to main content

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.

LayerResponsibilitiesMain classes
Presentation layerThe external interface, request and response conversion, input validation, authentication and authorizationEndpoint, request DTO, response DTO, Validator
Application layerExecuting use cases and coordinating domain servicesUseCase, Job
Domain layerBusiness rules, domain models, and interface declarationsModel, service interface, repository interface, exception
Infrastructure layerPersisting data, integrating with external systems, implementing the domain interfacesEntity, 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.

LayerClassNaming
Presentation layerEndpoint{Resource}Endpoint
Request DTO{Operation}Request
Response DTO{Operation}Response
Validator{Operation}Validator
Application layerUse case{Operation}UseCase
Batch job{JobName}Job
Domain layerDomain modelThe domain name itself (Order)
Service interface{DomainName}Service
Repository interface{DomainName}Repository
Factory{DomainName}ServiceFactory / {DomainName}RepositoryFactory
Infrastructure layerEntity{DomainName}Entity
DAO{DomainName}DAO
ImplementationStandard{DomainName}Service / Standard{DomainName}Repository
Default factoryStandard{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.

AspectEntityDomain model
LayerInfrastructure layerDomain layer
PurposeOne-to-one mapping to a database tableExpressing business logic
FieldsPublic fields (a requirement of the mapping)Private fields with getters
MutabilityMutable (values are set during mapping)Immutable preferred
ValidationNone (relies on database constraints)Implements business rules
ConversionConverted in the repository layerConverted 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.

LayerException classKindPurpose
Presentation layerValidationExceptionUncheckedA malformed request
Application layer{AppName}ExceptionCheckedA use case that failed
Domain layer{DomainName}ServiceException, RepositoryExceptionCheckedA business rule violation, or a failure to persist
Infrastructure layer and factories{DomainName}RuntimeExceptionUncheckedAn 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.

The conventions for laying out the source tree are covered in Module Project Structure.

Relationship to the script development model

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.