Application architecture
Each package in an application belongs to exactly one of the following layers:
- Business
- REST
- Core
- Store
The diagram below summarizes the package dependencies. For example, a class in a Core layer package can only reference classes in packages from the Store and Business layers.
Maven modules
How these packages map to Maven modules is not defined here.
For example, a given application may consist of dozens of Maven modules which only contain packages of the same layer. Another application may consist of only a few Maven modules, each containing packages for all layers.
Business layer¶
- The entities that the application works with are called (business) POJOs to differentiate them from DTOs (used inside the REST layer) and database record classes (used in the store layer).
- POJOs never reference application classes from other layers.
-
POJOs never reference classes/annotations from libraries or frameworks related to REST or persistence.
Rationale
This ensures that they can be passed around freely between layers without causing cycles in package or module dependencies.
-
POJO instances should be immutable. The preferred technique is to annotate the class with Lombok's
@Value
and@Builder
annotations:@Value @Builder(toBuilder = true) public class MyClass { }
Rationale
Immutable POJO classes not only facilitate thread-safe programming, but also prevent various mistakes: changing an instance's fields usually changes its hash code, which can break the behavior of any collection the instance is contained in. Another danger in altering an instance is that one may not be aware that another other part of the application still has a reference to that instance, but may not be built to deal with such changes.
REST layer¶
Classes in this layer are never referenced by application classes in another layer.
- Endpoint classes
-
Contains methods called by the container or framework to handle HTTP requests. Parameters of these methods are always DTOs or classes from libraries or the JDK, never business POJOs.
An endpoint class handles HTTP specifics like path mapping and accessing parameters, then calls the core layer to do the actual work.
Depending on the framework used, these are called REST controllers (Spring MVC), resource classes (JAX-RS) or servlet classes (Servlet Spec).
- DTO classes
-
Each DTO class represents an entity that is received from or sent to the client. Usually, there is a one-to-one mapping to business POJOs.
The fields in a DTO are usually a subset of the fields of the corresponding business POJO. Often, these fields use different types.
Details regarding JSON persistence are configured here.
- Converter classes
-
Used by the endpoint classes to turn incoming DTOs into POJOs and outgoing POJOs into DTOs.
Can be written manually, generated by tools or replaced by libraries. The important point is that the field mapping and conversion code does not reside in the endpoint classes or the DTOs.
Core layer¶
- Each business process is implemented by one class in this layer. That class performs its work by orchestrating other classes in the core and store layers.
- Class names consist of the associated business POJO and the process name. Common process names are
Creator
,Reader
,Updater
,Deleter
andLister
.
Examples:AccountCreator
,RecipeLister
. - Names are not dependent on other layers. For example, there usually is only one way to create a POJO, and the
corresponding class is the
Creator
. Even if that class uses anUpsertStore
, it is never calledUpserter
.
Store layer¶
Database record classes¶
- Database-oriented representation of business objects.
- For SQL databases, database record classes are usually generated at build time by jOOQ based on DDL scripts.
- Never referenced from other layers.
Store classes¶
- Each operation is implemented in its own class, be it the standard operations listed below or custom ones like "confirm message".
- To the business layer, store operations are atomic.
- For example, when implementing "Create" is only possible with a read before a write, those calls are encapsulated inside one store class.
-
The store operations for a given entity often implement certain guarantees, and those guarantees should be made explicit in the store operation APIs.
Corollary: no set of related store operations should make promises in their APIs that they (collectively) cannot guarantee.- For example, imagine a
User
business POJO where email addresses are converted to lowercase for storage and querying purposes. Therefore, we would write theUserCreateStore
andUserUpdateStore
classes in a way to prevent any two users from having email addresses which only differ by case. Consequently, theUserFindByEmailStore
can use a lowercase version of the given email address as its lookup criterion and reliably assume that there will be either no results or a single one.
Implementing store guarantees
It does not matter whether the guarantee is implemented using Java code in store classes, database features like unique indexes, or a combination of both. All that matters is that there is a real guarantee (and not just hope that humans will use the application in the expected way) and that it is indeed implemented in the store layer (and not some other layer).
- For example, imagine a
-
Internally, a store class may call other stores as needed.
Standard operations¶
Operation | Description |
---|---|
Create |
write a new row; throw ConflictingEntityException if it already exists |
Upsert |
write a new row, overwriting any existing one |
Update |
overwrite an existing row; throw ObjectNotFoundException if it does not exist |
Delete |
delete an existing row by primary key; throw ObjectNotFoundException if it does not exist |
DeleteBy* |
delete rows based on certain criteria that are not the primary key or only a subset of the primary key |
Read |
attempt to read a record and return an Optional |
FindBy* |
attempt to read one or multiple records based on certain criteria that are not the primary key or only a subset of the primary key, returning returning an Optional or a List |
List |
return a List of all records |
AtomicGroup* |
treat a group (Set or List ) of rows and some object that identifies this group as one atomic element for the store operation.For example, a FooAtomicGroupUpsertStore deletes existing Foo rows (if any) matching the given group key, then writes rows for the given group (which may be empty). |