Skip to content

Tomoka64/clean-architecture-java

Repository files navigation

Clean Architecture Java — Article Service

A REST API for creating, managing, and publishing articles. The project is built as a learning-focused Spring Boot application that demonstrates Clean Architecture, JWT authentication, role-based authorization, PostgreSQL persistence, database migrations, validation, and automated architecture tests.

Features

  • User registration with BCrypt password hashing
  • JWT login using HMAC SHA-256
  • Role-based access for readers, authors, editors, and administrators
  • Article draft, publish, and archive lifecycle
  • Ownership rules for updating, archiving, and deleting articles
  • Public paginated listing of published articles
  • Optional article filtering by category
  • PostgreSQL persistence and Flyway migrations
  • Consistent JSON error responses
  • Request logging with status, authenticated user, and duration
  • OpenAPI documentation and Swagger UI
  • Unit, web, context, and ArchUnit architecture tests

Technology

  • Java 17
  • Spring Boot 4.1
  • Spring Web MVC
  • Spring Security and OAuth2 Resource Server
  • Spring Data JPA and Hibernate
  • PostgreSQL 16
  • Flyway
  • Gradle
  • springdoc-openapi
  • JUnit 5, Mockito, MockMvc, and ArchUnit

Architecture

The application uses feature-oriented Clean/Hexagonal Architecture. Each feature is divided into domain, application, adapter, and configuration packages.

com.tomoka.articleservice
├── article
│   ├── domain
│   ├── application
│   │   ├── command
│   │   ├── model
│   │   └── port
│   │       ├── in
│   │       └── out
│   ├── adapter
│   │   ├── in
│   │   │   ├── transaction
│   │   │   └── web
│   │   └── out
│   │       └── persistence
│   └── config
├── auth
│   ├── application
│   ├── adapter
│   └── config
├── user
│   ├── domain
│   ├── application
│   ├── adapter
│   └── config
├── config
├── exception
└── filter

Dependencies point inward:

Web / Security / Persistence adapters
                 ↓
          Application ports
                 ↓
        Application services
                 ↓
               Domain

The domain and application packages contain no Spring, Jakarta, servlet, JPA, or JWT dependencies. Spring-specific transaction decorators surround the use cases, while persistence and security integrations implement outbound application ports.

ArchUnit tests enforce these boundaries during every test run.

Prerequisites

  • Java 17
  • Docker with Docker Compose

The project includes the Gradle wrapper, so a separate Gradle installation is not required.

Local setup

Create your environment file:

cp .env.example .env

Edit .env:

DB_PASSWORD=article_password
JWT_SECRET=replace-with-at-least-32-random-characters

You can generate a JWT secret with:

openssl rand -base64 32

Export the variables for the Spring Boot process:

set -a
source .env
set +a

Start PostgreSQL:

docker compose up -d

Start the application:

./gradlew bootRun

The API will be available at:

http://localhost:8080/api

Existing Docker volume

PostgreSQL applies POSTGRES_PASSWORD only when it initializes a new data directory. If the Docker volume already exists, DB_PASSWORD must match the password used when that volume was first created.

To remove the database and recreate it from scratch:

docker compose down -v
docker compose up -d

This command permanently deletes local database data.

Running from IntelliJ IDEA

IntelliJ does not automatically source .env. Add DB_PASSWORD and JWT_SECRET to the application Run Configuration before starting the service.

API documentation

After starting the application:

Authentication

The login endpoint returns a JWT valid for 24 hours. Protected requests must use the complete bearer authorization scheme:

Authorization: Bearer <token>

The JWT subject is the user's email. Its roles claim contains Spring authorities such as ROLE_AUTHOR.

Roles and permissions

Operation Public Reader Author Editor Admin
Register and log in Yes Yes Yes Yes Yes
List published articles Yes Yes Yes Yes Yes
Read an article by ID No Yes Yes Yes Yes
Create an article No No Yes No Yes
Update an article No No Own only No Any
Publish an article No No No Yes Yes
Archive an article No No Own only Any Any
Delete an article No No Own only No Any

New registrations receive the AUTHOR role. There is currently no public role-management endpoint.

Endpoints

All paths below are relative to /api.

Method Path Authentication Description
POST /v1/users Public Register a user
GET /v1/users/{id} Required Get a user
POST /v1/auth/login Public Log in and receive a JWT
GET /v1/articles Public List published articles
GET /v1/articles/{id} Required Get an article by ID
POST /v1/articles Author or admin Create a draft
PUT /v1/articles/{id} Owner or admin Update a draft
POST /v1/articles/{id}/publish Editor or admin Publish a draft
POST /v1/articles/{id}/archive Owner, editor, or admin Archive an article
DELETE /v1/articles/{id} Owner or admin Delete an article

Quick start with curl

1. Register

curl -X POST http://localhost:8080/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{
    "email": "author@example.com",
    "password": "password123",
    "displayName": "Example Author"
  }'

2. Log in

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

Example response:

{
  "token": "eyJ...",
  "tokenType": "Bearer",
  "expiresIn": 86400
}

If jq is installed, capture the token directly:

TOKEN=$(curl -sS -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "author@example.com",
    "password": "password123"
  }' | jq -r '.token')

3. Create an article draft

The author is derived from the authenticated JWT. Clients cannot choose an authorId.

curl -X POST http://localhost:8080/api/v1/articles \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Clean Architecture with Spring Boot",
    "summary": "A practical introduction to application boundaries.",
    "content": "Article content goes here.",
    "category": "Java"
  }'

4. Update a draft

curl -X PUT http://localhost:8080/api/v1/articles/1 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated title",
    "summary": "Updated summary.",
    "content": "Updated article content.",
    "category": "Java"
  }'

Only draft articles can be updated.

5. List published articles

curl "http://localhost:8080/api/v1/articles?page=0&size=20&category=Java"

The maximum page size is 100. Results default to sorting by publishedAt in descending order.

Article lifecycle

DRAFT ──publish──> PUBLISHED
  │                   │
  └────archive────────┴──> ARCHIVED
  • New articles always start as DRAFT.
  • Only drafts can be updated or published.
  • An archived article cannot be archived again.
  • Invalid state transitions return 409 Conflict.

Error responses

Application errors use a consistent JSON structure:

{
  "timestamp": "2026-07-13T12:00:00",
  "status": 404,
  "error": "Not Found",
  "message": "Article not found: 99",
  "path": "/api/v1/articles/99"
}

Common status codes:

  • 400 Bad Request — validation failed
  • 401 Unauthorized — missing or invalid authentication
  • 403 Forbidden — authenticated but not permitted
  • 404 Not Found — user or article does not exist
  • 409 Conflict — duplicate email or invalid article state

Database migrations

Flyway applies migrations from:

src/main/resources/db/migration

Hibernate uses ddl-auto=validate; it validates the entity mappings but does not create or modify tables. Add a new versioned Flyway migration whenever the database schema changes:

V2__add_article_slug.sql
V3__add_article_tags.sql

Do not modify a migration after it has been applied to a shared database.

Testing

The context test uses PostgreSQL, so start the Docker database before running the complete suite.

Run the complete suite:

./gradlew test

Run a clean build:

./gradlew clean test

The test suite covers:

  • Domain lifecycle behavior
  • Application authorization and ownership rules
  • User registration and email normalization
  • Login orchestration
  • Controller validation and authenticated identity mapping
  • JWT role conversion
  • Spring application context and persistence wiring
  • Clean Architecture dependency rules

Test reports are generated under:

build/reports/tests/test/index.html

Request logging

Every request logs its method, path, response status, authenticated user, and duration:

HTTP POST /api/v1/articles status=200 user=author@example.com durationMs=42

Authorization headers, JWT values, passwords, request bodies, and query strings are intentionally not logged.

Configuration

Property Environment variable Default
Database URL jdbc:postgresql://localhost:5432/articlesdb
Database username article_user
Database password DB_PASSWORD Required
JWT secret JWT_SECRET Required; minimum 32 characters
JWT expiration 24h
Context path /api
Maximum page size 100

Production deployments should provide database credentials and JWT secrets through a secret manager or deployment environment rather than committed files.

Useful commands

# Start PostgreSQL
docker compose up -d

# View PostgreSQL logs
docker compose logs -f postgres

# Stop containers
docker compose down

# Run the application
./gradlew bootRun

# Run tests
./gradlew clean test

About

Clean Architecture in Java

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages