Project Layout Cheatsheet (edges β core)¶
A compact, repeatable map for multi-entity Spring Boot apps. Use vertical slices (features) with the same rhythm in each slice: web β app β domain, plus infra and resources.
Top-level¶
<app>/
ββ build.gradle | pom.xml
ββ README.md
ββ .editorconfig
ββ .gitignore
ββ src/
Keep the repo boring: one app per repo until you truly need multiple deployables.
Package skeleton (repeat per slice)¶
src/main/java/com/edge/<app>/
ββ web/ # HTTP edge: controllers, DTOs, ProblemDetail
β ββ {slice}/
β ββ {Slice}Controller.java
β ββ dto/
β β ββ Create{Thing}Request.java
β β ββ Update{Thing}Request.java
β β ββ {Thing}Response.java
β ββ mapper/
β ββ {Slice}WebMapper.java # MapStruct, edge-owned mappings
ββ app/ # Use-cases (application layer)
β ββ {slice}/
β ββ Create{Thing}UseCase.java
β ββ Update{Thing}UseCase.java
β ββ Query{Thing}UseCase.java
β ββ {Slice}Service.java # @Service, @Transactional, implements interfaces
ββ domain/ # Entities + repository ports (interfaces)
β ββ {slice}/
β β ββ {Thing}.java
β β ββ {Thing}Repository.java
β ββ common/
β ββ BaseEntity.java
β ββ Money.java / Quantity.java # value objects (optional)
β ββ DomainEvent.java (optional)
ββ infra/ # Adapters and wiring
ββ persistence/
β ββ JpaConfig.java
β ββ SpecBuilders.java # Spring Data Specifications, projections
ββ mapping/
β ββ MapStructConfig.java # global mapper rules
ββ security/
ββ SecurityConfig.java # when you add auth
Slices are feature folders like catalog/, inventory/, sales/, procurement/. Add more as the domain grows.
Resources & migrations¶
src/main/resources/
ββ application.yml
ββ application-<profile>.yml # local, test, prod
ββ db/migration/
β ββ V1__init_<slice>.sql
β ββ V2__add_<constraint>.sql
β ββ ...
ββ logback-spring.xml
Truth lives in Flyway. Entities follow the SQL, not vice-versa.
Error handling (edge)¶
web/error/
ββ GlobalExceptionHandler.java # @RestControllerAdvice β ProblemDetail
ββ ProblemTypes.java # stable type URIs (use properties for base URL)
- Translate exceptions once at the web edge.
- Use stable
typeURIs like/problems/{slice}/{problem}; base set via properties.
Tests mirror main¶
src/test/java/com/edge/<app>/
ββ web/{slice}/{Slice}ControllerIT.java # @SpringBootTest + MockMvc
ββ app/{slice}/{Slice}ServiceTest.java # unit tests (Mockito) or @DataJpaTest if needed
ββ domain/{slice}/{Thing}RepositoryIT.java # @DataJpaTest
src/test/resources/application-test.yml # H2 or Testcontainers
Responsibilities & allowed directions¶
-
web/ Speaks HTTP and DTOs. Handles validation, ProblemDetail, headers (
Location, caching). May depend on:app,domain(types only),infra.mapping(MapStruct config). Never: return entities, call repositories directly. -
app/ Orchestrates use-cases, transactions, cross-entity rules. Throws domain/app exceptions. May depend on:
domain(entities, repositories). Never: know about HTTP, controllers, servlet stuff. -
domain/ Entities, value objects, repository ports (Spring Data interfaces are fine). Self-contained invariants. No Spring web annotations. Never: know about
weborinfra. -
infra/ Adapters (DB config, messaging, security), technical mappings/projections. May depend on:
domainfor entity classes. Never: call controllers or use DTOs.
Think downward-only: web β app β domain (and infra supports the stack).
Naming rules (keep it predictable)¶
- DTOs:
{Action}{Thing}Request(input),{Thing}Response(output). Scenario-first names:AdjustStockRequest, notUpdateWarehouse. - Use-cases: verbs:
CreateProductUseCase,ListProductsUseCase. Services implement multiple use-cases per slice:CatalogService implements CreateProductUseCase, ... - Repositories:
{Thing}Repository. No βDAOβ suffixes. - Controllers:
{Thing}Controlleror{Slice}Controllerif it aggregates.
Mappers (at the edge)¶
- Web mappers live under
web/{slice}/mapper. They convert DTO β Command and Entity β Response. - Global defaults in
infra/mapping/MapStructConfig.java:
@MapperConfig(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE
)
Annotate each mapper with @Mapper(config = MapStructConfig.class).
Properties & configuration¶
- Put base URIs, feature flags, cache TTLs in
application.yml. - Bind via
@ConfigurationProperties(prefix = "app")classes underinfra/orweb/config/(if HTTP-specific). - Profiles:
local,test,prod. Never commit secrets to YAML.
Where future files go (decision table)¶
| Thing youβre adding | Folder | Notes |
|---|---|---|
| New HTTP endpoint (CRUD, search) | web/{slice}/{Thing}Controller.java |
DTOs + web mapper in same slice |
| Input/Output models for that endpoint | web/{slice}/dto/ |
Validate with Bean Validation |
| Mapping between DTOs and entities | web/{slice}/mapper/ |
Use MapStruct |
| New business workflow | app/{slice}/ |
Add *UseCase + implement in service |
| Cross-entity rule (e.g., stock never negative) | app/{slice}/{Slice}Service.java |
Enforce in a @Transactional use-case |
| New entity | domain/{slice}/{Thing}.java |
Keep invariants here |
| Repository for entity | domain/{slice}/{Thing}Repository.java |
Spring Data interface |
| DB constraint / table | resources/db/migration/V*__*.sql |
One concern per migration |
| Global Jackson/CORS tweaks | web/config/WebConfig.java |
Keep edge-only concerns here |
| ProblemDetail mapping for new exception | web/error/GlobalExceptionHandler.java |
Add a method + constant in ProblemTypes |
| Spring Security config | infra/security/SecurityConfig.java |
Keep HTTP authorizations in web annotations if needed |
| Projections/Specifications | infra/persistence/ |
Donβt leak to web/app |
| Domain value object (Money, Quantity) | domain/common/ |
Immutable, validated |
| Scheduled job / batch | app/{slice}/ or infra/ (adapter) |
Use-case in app, triggers in infra |
| Integration client (SMTP, S3, Kafka) | infra/{integration}/ |
One subpackage per integration |
Adding a new slice (recipe)¶
- Create
domain/{slice}with entities + repositories. - Create
app/{slice}with use-case interfaces +{Slice}Service. - Create
web/{slice}with controller, DTOs, mapper. - Add Flyway migration(s).
- Add tests mirroring
main.
You can ship only web+app+domain for a slice; infra stays minimal until needed.
When to split things further¶
- A service file passes ~400β500 lines or mixes unrelated workflows β split by verb or read/write (
OrderCommandServicevsOrderQueryService). - DTO package gets crowded β subfolders per aggregate (
dto/product/*,dto/category/*). - Query logic turns gnarly β move complex queries to Specifications/Query classes in
infra/persistence.
Guardrails (the βdonβt get weirdβ list)¶
- Donβt return entities from controllers. Always map to responses.
- Donβt inject repositories into controllers. Controllers talk to use-cases only.
- Donβt let
webknow about JPA annotations; donβt letdomainknow about HTTP. - Donβt put shared junk in
domain/common/. Only true, reusable domain primitives belong there.
Tiny examples (paths)¶
-
Create product endpoint
web/catalog/ProductController#createDTOs inweb/catalog/dto/CreateProductRequest.javaMapper inweb/catalog/mapper/ProductWebMapper.javaUse-caseapp/catalog/CreateProductUseCase.javaβ implemented byCatalogService. -
Adjust stock
web/inventory/StockItemController#adjustAdjustStockRequestβInventoryService.adjustStock(...)βStockRepository.