Choosing an architecture
One model, three readings. The architecture word in the elasticore { } block picks the bundle;
the model stays as it is.
elasticore {
architecture = 'hexagonal' // layered | ddd | hexagonal
language = 'java' // java | kotlin
}
What each one gives
| Layered | DDD | Hexagonal | |
|---|---|---|---|
| Java bundle | jpa-springboot (-Pv3: jpa-springboot-v3) | ddd-springboot | hexagonal-springboot |
| Kotlin bundle | jpa-springboot-kt | ddd-springboot-kt | hexagonal-springboot-kt |
| Entities, DTOs, mapper, repository, services, controllers | yes | yes | yes |
| Repository, service, controller for | every exposed entity | aggregate roots only | aggregate roots only |
@access, @owner, @password, @login, @signin enforced | Java: not in the default 2.x-compatible output (the generator warns); -Pv3 and Kotlin: yes | yes | yes |
Lifecycles (states:), @snapshot, @compute | no | yes | yes |
Invariants, factories, specifications, domain events, commands: | no | yes | yes |
Sealed roots (XxxBase + your Xxx), @editable | no | yes | yes |
References to other roots by id (customerId) | no | yes | yes |
| Repository port in the domain + JPA adapter | no | yes | yes |
Inbound port <Entity>UseCase, controllers inject it | no | no | yes |
Problem+json error answers (XxxApiErrors) | -Pv3 and Kotlin | yes | yes |
How to choose
- A new project:
hexagonalorddd. They enforce everything the DSL can say - access, ownership, lifecycles, derived values - in regenerated code, so a rule holds at every entry point. The pet store, the quick start and the agent kit's recipes usehexagonal. hexagonalwhen you want the application's surface as an interface (OrderUseCase) and the packages laid out as ports and adapters (application.port.in,adapter.in.web,infrastructure.persistence).dddfor the same domain model with the controller calling the application service directly.layeredfor a 2.x project moving to 3.0: in Java it is the 2.x-compatible output on the rule engine. Add-Pv3for the cleaned-up output with access checks and problem+json answers.
Where each kind of file goes is set in env.yml (namespace:), not by the bundle. namespace: base: com.acme.shop lays out the hexagonal packages under one base; any key written next to it wins. The
bundles decide what is generated; the namespaces decide where.
Changing the architecture later
Changing architecture is a decision: the files generated per root change (a split root, a use
case, id references), and the generate-once files you already own are not rewritten. The generator
tells you what it cannot rewrite - for example, an extension service whose constructor no longer
matches gets the warning constructor out of date with its core service - the generator now calls it with (...).