Skip to content

About

Product management REST API in Java 21 and Spring Boot 3, built with Clean/Hexagonal Architecture across 4 Maven modules (domain, application, infrastructure, api). PostgreSQL with JPA and Flyway migrations, soft delete, paginated price filtering, Bean Validation, RFC-7807 errors, and unit + integration tests with JUnit 5 and Testcontainers.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

4 Commits

Folders and files

Repository files navigation

Spring Boot Clean Architecture — Product API

Java Spring Boot PostgreSQL Maven OpenAPI Tests

A product management REST API built with Java 21 and Spring Boot 3, structured with Clean / Hexagonal Architecture (Ports & Adapters) across four independent Maven modules. The goal is a small but realistic codebase that takes the dependency rule seriously: the domain knows nothing about frameworks, persistence, or HTTP.


Architecture

Four Maven modules, each with a single responsibility and a strict dependency direction:

flowchart TD
    API["api<br/><i>REST controllers, HTTP DTOs</i>"] --> APP["application<br/><i>use cases, ports, command/result DTOs</i>"]
    INFRA["infrastructure<br/><i>JPA, Flyway, adapters</i>"] --> APP
    APP --> DOM["domain<br/><i>entities, value objects, business rules</i>"]
    INFRA --> DOM
Loading

Dependency rule

api  →  application  →  domain
infrastructure  →  application  →  domain
  • domain — pure Java. No Spring, no JPA, no annotations. Entities (Product), value objects (Money, ProductId), and business rules live here.
  • application — orchestrates the domain. Defines input ports (interfaces the API calls) and output ports (interfaces the infrastructure implements). Use cases are the only place transactions are opened.
  • infrastructure — implements the output ports with Spring Data JPA, maps domain ↔ JPA entities, and owns the database schema via Flyway.
  • api — Spring MVC controllers, HTTP request/response records, validation, and centralized error handling. Talks only to the application's input ports.

The domain is testable without a Spring context, and the persistence technology could be swapped by touching only infrastructure.


Tech stack

Concern Technology
Language Java 21 (records, sealed-friendly, pattern matching)
Framework Spring Boot 3.5 (Web, Data JPA, Validation)
Persistence PostgreSQL 16, Hibernate, Spring Data JPA
Migrations Flyway
Build Maven (multi-module reactor)
Boilerplate Lombok
Errors RFC 7807 ProblemDetail
Testing JUnit 5, Mockito, AssertJ, Testcontainers

Features

  • Full CRUD for products over a clean REST API (/api/v1/products).
  • JWT authentication with public registration/login endpoints, BCrypt password hashing and stateless protection for product routes.
  • Soft delete + revive — deleting flips an active flag instead of removing the row; creating a product whose name belongs to a soft-deleted row revives that row instead of failing (see Design decisions).
  • Pagination + price filtering — GET supports minPrice / maxPrice ranges and page/size, with a server-side cap of 50 items per page to protect memory.
  • Uniqueness, defended in depth — a duplicate-name check in the use case (friendly 409) backed by a database UNIQUE constraint (the real guarantee, race-proof).
  • Bean Validation on request bodies, surfaced as 422 with a list of field errors.
  • Centralized error handling with @RestControllerAdvice mapping domain/application exceptions to proper HTTP statuses via ProblemDetail.
  • Versioned schema with Flyway migrations.
  • Interactive API docs — OpenAPI 3 + Swagger UI (springdoc), auto-generated from the controllers.
  • Unit + integration tests — domain, authentication use cases and JWT with JUnit/Mockito; security rules with MockMvc; repository against a real PostgreSQL via Testcontainers.

Project structure

.
├── domain/
│   └── entity, valueobject, exception
├── application/
│   └── usecase, port/in, port/out, dto, exception
├── infrastructure/
│   └── persistence (entity, repository, mapper), db/migration
├── api/
│   └── controller, dto, mapper, exception, CleanArchitectureApplication
├── docker-compose.yml
└── pom.xml

Getting started

Prerequisites

  • JDK 21+
  • Maven 3.9+
  • Docker (for PostgreSQL and the integration tests)

Run

docker compose up -d

mvn spring-boot:run -pl api

docker compose down

On startup, Flyway applies the migrations and Hibernate validates that the entities match the schema.

Tests

mvn test
mvn verify

API reference

Once the app is running, the full API is documented and explorable at:

Authentication base path: /api/v1/auth

Method Path Description Success Errors
POST /register Register a user 201 409 duplicate email, 422 validation
POST /login Authenticate and issue a JWT 200 401 invalid credentials, 422 validation

Products base path: /api/v1/products. All product routes require Authorization: Bearer <access-token>.

Method Path Description Success Errors
POST / Create a product (revives a soft-deleted one with the same name) 201 409 duplicate, 422 validation
GET / List active products — paginated, optional price range 200 —
GET /{id} Find a product by id 200 404
PUT /{id} Update name and price 200 404, 409, 422
DELETE /{id} Soft delete (sets active = false) 204 404

List query parameters: minPrice, maxPrice (both optional), page (default 0), size (default 20, max 50).

Examples

curl -X POST http://localhost:8080/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"strongpass123"}'

curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"strongpass123"}'

curl -X POST http://localhost:8080/api/v1/products \
  -H "Authorization: Bearer <access-token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Notebook Dell","price":4999.90}'

curl "http://localhost:8080/api/v1/products?minPrice=100&maxPrice=1000&page=0&size=20" \
  -H "Authorization: Bearer <access-token>"

curl -X PUT http://localhost:8080/api/v1/products/{id} \
  -H "Authorization: Bearer <access-token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Notebook Dell XPS","price":5999.90}'

curl -X DELETE http://localhost:8080/api/v1/products/{id} \
  -H "Authorization: Bearer <access-token>"

Validation error (422) is returned as ProblemDetail:

{
  "type": "about:blank",
  "title": "Validation Error",
  "status": 422,
  "errors": ["price: price must be positive"]
}

Design decisions

A few choices that go beyond a textbook CRUD:

  • Soft delete that doesn't burn names. A soft-deleted row keeps its name, which would otherwise collide with the global UNIQUE(name) constraint forever. Instead of a partial index, creating a product with a soft-deleted name revives the existing row (reactivates it and updates the price), so there is never a duplicate and names stay reusable.
  • Pagination without leaking the framework. Spring Data's Page / Pageable stay inside infrastructure. The application exposes its own PageResult<T> record, so the domain and application modules never depend on Spring Data — they couldn't even compile against it. The page size is clamped (1–50) in the use case, where the business rule belongs.
  • Memory vs. performance, separated. The LIMIT on the query is what protects memory (the database returns at most 50 rows regardless of table size). Page adds a count(*) query for total pages — a deliberate, documented cost.

Roadmap

  • Additional list filters (by name, created date) and sorting options
  • CI pipeline (GitHub Actions running mvn verify)

Author

Marcelo Alves — built as a hands-on study of Clean Architecture with Spring Boot, coming from a .NET / C# background.

About

Product management REST API in Java 21 and Spring Boot 3, built with Clean/Hexagonal Architecture across 4 Maven modules (domain, application, infrastructure, api). PostgreSQL with JPA and Flyway migrations, soft delete, paginated price filtering, Bean Validation, RFC-7807 errors, and unit + integration tests with JUnit 5 and Testcontainers.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages