Spring Boot project layout: single slice example¶
shopping-cart/
ββ build.gradle / pom.xml
ββ README.md
ββ .editorconfig
ββ .gitignore
ββ src
ββ main
β ββ java/com/edge/shopping_cart
β β ββ web/ # HTTP edge (controllers, request/response DTOs, exception translators)
β β β ββ category/
β β β β ββ CategoryController.java
β β β β ββ dto/
β β β β β ββ CreateCategoryRequest.java
β β β β β ββ CategoryResponse.java
β β β β ββ mapper/ # MapStruct mappers AT THE EDGE
β β β β ββ CategoryWebMapper.java
β β β ββ error/
β β β β ββ GlobalExceptionHandler.java # @RestControllerAdvice β ProblemDetail
β β β β ββ ProblemTypes.java # optional: catalog of problem "type" URIs / codes
β β β ββ config/
β β β ββ WebConfig.java # CORS, Jackson tweaks, message converters if needed
β β ββ app/ # Use-cases (application layer): orchestrates domain + repos
β β β ββ category/
β β β β ββ CreateCategoryUseCase.java
β β β β ββ UpdateCategoryUseCase.java
β β β β ββ DeleteCategoryUseCase.java
β β β β ββ CategoryService.java # @Service, @Transactional, implements use-cases
β β β ββ exception/
β β β ββ CategoryNotFoundException.java
β β β ββ DuplicateCategoryException.java
β β ββ domain/ # Business objects + repository ports (interfaces)
β β β ββ category/
β β β β ββ Category.java
β β β β ββ CategoryRepository.java # Spring Data interface (port)
β β β ββ common/
β β β ββ BaseEntity.java # optional: id, createdAt, updatedAt
β β ββ infra/ # Adapters: DB config, security, integrations (SMTP, S3, etc.)
β β ββ persistence/
β β β ββ JpaConfig.java # Naming strategies, batch size, etc. (if needed)
β β ββ security/
β β β ββ SecurityConfig.java # Only if/when you add security
β β ββ mapping/
β β ββ MapStructConfig.java # Global MapStruct defaults (optional)
β ββ resources
β ββ application.yml
β ββ application-local.yml # local overrides
β ββ db/migration/ # Flyway: V1__init.sql, V2__add_unique_category.sql, ...
β ββ logback-spring.xml # sane logging (JSON or pattern)
ββ test
ββ java/com/edge/shopping_cart
β ββ web/category/
β β ββ CategoryControllerIT.java # @SpringBootTest + @AutoConfigureMockMvc
β ββ app/category/
β β ββ CategoryServiceTest.java # @DataJpaTest or @ExtendWith(SpringExtension)
β ββ domain/category/
β ββ CategoryRepositoryIT.java # @DataJpaTest
ββ resources/
ββ application-test.yml # H2 or Testcontainers config
Why this works (and where each thing lives)¶
-
web/is the outermost HTTP shell: -
Controllers, request/response DTOs,
@RestControllerAdvice, and edge mappers (MapStruct). - Only talks in HTTP types and DTOs; never leaks JPA entities back to clients.
-
app/holds the use cases and application rules: -
*UseCaseinterfaces +CategoryServiceimplementation. - Throws domain/application exceptions. Mark methods
@Transactional. -
domain/is the boring, stable core: -
Entities and repository interfaces (ports).
- Keep it persistence-agnostic in spirit; Spring Data lives here fine because itβs an interface.
-
infra/is wiring and adapters: -
JPA config, security, external systems (email, S3, payment, Kafka).
- When you add an external integration, create a subpackage per integration.
-
resources/db/migration/is Flyway: -
SQL scripts define schema truth. The entity should match the migration, not the other way around.
-
test/mirrorsmain/: -
Unit tests for
app/services, slice tests for repos (@DataJpaTest), and integration tests for controllers.
Category example (where your files go)¶
web/category/dto/CreateCategoryRequest.javaweb/category/dto/CategoryResponse.javaweb/category/mapper/CategoryWebMapper.javaweb/category/CategoryController.javaapp/category/CreateCategoryUseCase.java,UpdateCategoryUseCase.java,CategoryService.javaapp/exception/DuplicateCategoryException.java,CategoryNotFoundException.javadomain/category/Category.java,CategoryRepository.javaweb/error/GlobalExceptionHandler.java(translates exceptions β ProblemDetail)
MapStruct: edge mappers live at the edge¶
Keep mappers with the layer that owns the types:
- Web mappers in
web/.../mapperconvertRequest β CommandandEntity β Response. - If you ever add persistence DTOs (e.g., raw projections), add a mapper in
infra/mapping/for that boundary.
Optional global config:
// infra/mapping/MapStructConfig.java
@Configuration
@MapperConfig(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE
)
public class MapStructConfig {}
Then each mapper: @Mapper(config = MapStructConfig.class).
ProblemDetail handler location¶
- Put
GlobalExceptionHandlerinweb/error/. - It depends on HTTP concerns (
ProblemDetail,HttpStatus), so itβs an edge concern.
Configuration split¶
application.ymlβ defaults.application-local.ymlβ dev overrides (DB URL, logging).application-test.ymlβ tests (H2 or Testcontainers).- Use Spring profiles (
local,test,prod).
Flyway starter pack¶
src/main/resources/db/migration/V1__init.sql
V2__trim_ci_unique_index.sql (optional hardening)
-- Postgres example: case/trim-insensitive index
CREATE UNIQUE INDEX uq_category_name_norm ON category (lower(btrim(name)));
Testing layout (pragmatic)¶
- Controller IT:
@SpringBootTest+ MockMvc, assertLocationheader and JSON body. - Service tests: plain JUnit + Mockito or
@DataJpaTestif you want real DB behavior. - Repository tests:
@DataJpaTestto lock in query behavior/constraints.
Package names vs. folders¶
Java packages mirror folders 1:1. Keep them short and semantic:
com.edge.shopping_cart.web.categorycom.edge.shopping_cart.app.categorycom.edge.shopping_cart.domain.categorycom.edge.shopping_cart.infra.persistence
TL;DR rules of placement¶
- HTTP stuff (controllers, ProblemDetail, web mappers) β
web/ - Use cases + transactions + domain exceptions β
app/ - Entities + repository interfaces β
domain/ - DB/security/external adapters/config β
infra/ - Schema β
resources/db/migration/ - Tests mirror main