usermanager is a PoC of a low-ceremony, anti-indirection Spring application using a relational
database through Jooq.
The domain consists of three entities: Users, Projects, and Organizations.
Those entities form the intuitive relationships:
- Users and Organizations are in a many-to-many relationship tagged with a Role,
- Users and Projects are in a many-to-many relationship,
- Projects and Organizations are in a many-to-one relationship.
This experiment in code organization is motivated by two fragments from Eric Evans' "Domain-Driven Design":
For data centric operations you would probably be better off using something like an Active Record pattern, or even a DAL over stored procedures. You may find some benefits in some of the more cursory aspects of DDD, and perhaps using some of the terminology, but trying to make DDD fit here will not be a pleasant experience.
and
Probably 95% of all software applications fall into the “not so good for using DDD” categories. Most are fundamentally Data Centric – most websites are, most desktop applications are … fundamentally most data updating and reporting applications are data centric.
... and X clearly is data-centric.
The guiding principle behind code organization in this project is the observation that a REST-ful API is organized mostly around its endpoints. An endpoint is made up of several closely-coupled classes, and apart from occasional common classes, its logic is mostly independent of other endpoints. Of course, the data access layer (repositories, entities) is shared by most endpoints and as such it deserves to be factored out into a common module.
The "strong cohesion, low coupling" principle suggests organizing code around vertical, endpoint-specific modules:
Spring REST / Swagger API
--------------------------------------------------------------------------------|
endpoint 1: | endpoint 2: | ... | endpoints 3: | Some
- just one layer | - first layer | | - first layer | common
| - second layer | | - second and final | classes
| - third and final | | |
-------------------------------------------------------------------------------------------------------
Data access layer (mostly Jooq-generated)
Contrast this with the current, horizontally-layered approach of X:
Spring REST / Swagger API
-------------------------------------------------------
API-domain conversion
-------------------------------------------------------
Access control and anti-corruption layer (services)
-------------------------------------------------------
Shared or endpoint-specific domain logic (interactors)
-------------------------------------------------------
Hand-written repositories and domain-Jooq conversion
-------------------------------------------------------
Data access layer generated by Jooq
The proposed structure can be better:
- it makes a clear distinction between (A) focused, tighly-coupled endpoint- / resource-specific code, and (B) common classes with focused, clear interfaces and contracts
- it allows each endpoint to use as many or as few layers as needed
- it fully exploits Jooq's capabilities for generating repositories and POJOs
- it does not obfuscate X's fundamentally data-centric nature behind a redundant caricature of DDD in the domain layer
- [perhaps most important] code in
implcan be written faster and with less perfectionism: endpoint implementations are strictly independent and self-contained, so less-than-perfect code in one endpoint will not spill into other places
The code in organized in four packages, whose dependency graph is sketched below:
api
|
impl
/ \
/ common
| /
database
Those packages are intended to be used like this:
-
The
apipackage contains Spring REST controllers and API models with Swagger annotations. It depends on theimplpackage for implementation of endpoints. -
The
implpackage contains implementations of endpoints.
The intention is that each endpoint should correspond to one sub-package of impl.
Endpoint implementations can depend on common and database packages, but they
should not depend on each other. Reusable code should live in the common package.
An exception to the "one endpoint, one package" rule is when a group of endpoints implement CRUD-esqe operations on a resource. CRUD operations on a single resource should belong in the same package, named after the resource.
-
The
commonpackage contains classes which can reused between endpoints. The intention is that those classes should have a focused, well-defined interfaces and contracts. It depends on thedatabasepackage for database access. -
The
databasepackage contains classes which are needed for Jooq code generation. Classes generated by Jooq also belong to this package. It does not depend on other packages.
Of course, more packages can be added if other infrastructure components are used.
If we decide to adopt some elements of the proposed approached, new and existing features can be incrementally migrated to use this endpoint-centric approach.
This could be done by introducing the new, vertical package structure to a component, and implement new features in that vertical structure (or refactor existing features to use this structure). The vertical structure could co-exist with the old, horizontal structure, possibly until the later is fully replaced or retired.
Furthermore, endpoints written with the new approach could use a different version of Jooq-generated code (e.g. with Repositories aka DAOs and POJOs). If this Jooq-generated code is not added to version control, and instead generated as part of the build process (this is the canonical way to use Jooq), such duplication of Jooq code would not result in bigger diffs in version control.
Hopefully, adopting elements of this new approach could speed up delivery of features. It could be that fewer unnecessary levels of indirection and fewer attempts to abstract code that will never be abstracted will leave more room for creating features for end-users.
To prove the point, have a look at the graph of classes which had to be modified to add a new endpoint as part of my [Piotr J's] recent task. The task was to add an endpoint for setting the username, communicating with the database and Keycloak in the process.
- RegistrationApiController
- RegistrationApi
- RegistrationApiImpl
- RegistrationService
- RegistrationServiceImpl
- AuthenticationService
- KeycloakAuthenticationService
- OAuthApiClient
With the proposed approach, we could use as many levels of indirection as needed. Perhaps:
- RegistrationApiController
- SetUsernameEndpoint // class name TBD
- // possibly one more indirection level if needed
- KeycloakService // endpoint-specific, factored out into a class so that it can be stubbed
There are possible settings in which our current horizontally-layered, pure-DDD, infrastructure-agnostic approach would be a good fit:
- When infrastructure is likely to fundamentally change before application code fundamentally changes (HTTP to gRPC, relational database to Cassandra) - not the case for X.
- When the business domain of the product has complex rules, and it has to be modelled in ubiquitious language, which is independent of the data layer and understandable to non-technical domain experts - not the case for X.
- When there are multiple teams collaborating on code within a single bounded context (single component or database) and different teams take care of different aspects of the domain model: different infrastructures etc. - not the case for X.
- When the team is likely to fundamentally change (most team members go) before application code fundamentally changes
- not the case for X.
When I assume that those circumstanced are not the case for X, I mean that our requirements and technology stacks are changing so fast (new domain, rewrite of the Leaderboard, pending rewrite of the Backend), that our application code is unlikely to outlive either the infrastructure choices or the team itself.