A RESTful task management API built with Spring Boot 4.x and PostgreSQL, containerised with Docker Compose. This project was built milestone-by-milestone as a learning exercise covering the full lifecycle of a production-ready Spring Boot service.
| Technology | Version | Role |
|---|---|---|
| Java | 21 | Language / runtime |
| Spring Boot | 4.0.6 | Application framework |
| PostgreSQL | 16 | Relational database |
| Flyway | (managed by Spring Boot) | Database migrations |
| Docker | 24+ | Container runtime |
| Docker Compose | v2 | Multi-container orchestration |
| SpringDoc OpenAPI / Swagger UI | 3.0.3 | API documentation |
| Lombok | (managed by Spring Boot) | Boilerplate reduction |
| Maven | 3.9+ | Build tool |
Layered design — request flow through Controller, Service, and Repository
Local development via Docker Compose — two containers on a private network
Target production deployment — EC2 hosting the Docker container, RDS for the database
- Java 21 — required to build the project locally (not needed if you only use Docker)
- Docker Desktop — includes the Docker daemon and Docker Compose v2
- Docker Compose v2 — verify with
docker compose version
-
Clone the repository
git clone <repo-url> cd task-manager
-
Create the environment file
cp .env.example .env # or create .env manuallyThe
.envfile must contain:DB_PASSWORD=changeme_in_production
Change
changeme_in_productionto a strong password before deploying anywhere beyond your local machine. -
Build and start all services
docker compose up --build
Docker Compose will:
- Build the Spring Boot image using the multi-stage Dockerfile
- Start the PostgreSQL container and wait for it to pass its
pg_isreadyhealth check - Start the API container; Flyway runs migrations automatically on startup
-
Verify the API is healthy
curl http://localhost:8080/actuator/health
Expected response:
{"status":"UP"} -
Explore the API with Swagger UI
Open your browser at: http://localhost:8080/swagger-ui.html
-
Stop the services
docker compose down
To also remove the database volume (destroys all data):
docker compose down -v
Base path: /api/v1/tasks
| Method | Path | Description | Success Status |
|---|---|---|---|
POST |
/api/v1/tasks |
Create a new task | 201 Created |
GET |
/api/v1/tasks |
List tasks (filtering, sorting, pagination) | 200 OK |
GET |
/api/v1/tasks/{id} |
Get a single task by UUID | 200 OK |
PUT |
/api/v1/tasks/{id} |
Replace a task with new data | 200 OK |
DELETE |
/api/v1/tasks/{id} |
Delete a task by UUID | 204 No Content |
| Parameter | Type | Default | Description |
|---|---|---|---|
status |
TaskStatus |
— | Filter by status: TODO, IN_PROGRESS, DONE |
priority |
TaskPriority |
— | Filter by priority: LOW, MEDIUM, HIGH |
sortBy |
string |
createdAt |
Field to sort by (e.g. title, dueDate, priority) |
sortDir |
string |
asc |
Sort direction: asc or desc |
page |
int |
0 |
Zero-based page number |
size |
int |
20 |
Page size (max: 100) |
curl "http://localhost:8080/api/v1/tasks?status=TODO&priority=HIGH&sortBy=dueDate&sortDir=asc&page=0&size=10"Set up a Spring Boot project from scratch using Spring Initializr and Maven.
Key concepts:
- Spring Boot auto-configuration — the framework inspects the classpath and wires beans automatically (e.g.
DataSource,EntityManagerFactory) without XML config. - Maven dependency management — the
spring-boot-starter-parentBOM pins compatible versions for all Spring dependencies, eliminating version conflicts. - Actuator health endpoint —
spring-boot-starter-actuatorexposes/actuator/healthout of the box, giving a simple liveness check that Docker and load balancers can poll.
Introduced Flyway to manage the database schema as versioned SQL scripts.
Key concepts:
- Database versioning — each migration file (
V1__,V2__,V3__) is applied exactly once and recorded in theflyway_schema_historytable, giving a full audit trail of schema changes. - Idempotent migrations — once applied, a migration is never re-run. This makes deployments safe to repeat and rollback strategies explicit.
- Schema evolution without downtime — additive changes (new columns with defaults, new tables) can be applied while the old application version is still running, enabling zero-downtime deploys.
Modelled the Task entity and its associated DTOs.
Key concepts:
- ORM mapping — JPA annotations (
@Entity,@Table,@Column,@Enumerated) map Java classes to database tables without writing SQL for CRUD operations. - Lombok boilerplate reduction —
@Data,@Builder,@NoArgsConstructor, and@AllArgsConstructorgenerate getters, setters,equals,hashCode, and constructors at compile time, keeping entity classes concise. - Java records for immutable DTOs —
recordtypes (e.g.TaskResponse,CreateTaskRequest) are immutable by design, making them ideal for data transfer where mutation is undesirable. - Enum mapping with
EnumType.STRING— storingTaskStatusandTaskPriorityas strings (TODO,HIGH) rather than ordinals makes the database readable and resilient to enum reordering. @ManyToOnerelationship —Taskhas a nullable foreign key toUserwithON DELETE SET NULL, meaning deleting a user orphans their tasks rather than cascading the delete.OffsetDateTimefordueDate— stores timezone offset alongside the timestamp, avoiding ambiguity when clients are in different time zones.
Implemented the business logic layer between the controller and the repository.
Key concepts:
- Repository pattern —
TaskRepositoryextendsJpaRepository, providing standard CRUD methods without any SQL. The service layer depends on the interface, not the implementation. - JPA Specifications for dynamic queries —
TaskSpecificationsbuildsPredicateobjects at runtime to filter bystatusandprioritywithout writing multiple query methods or raw SQL. - Pagination with Spring Data —
PageableandPage<T>handle offset/limit, total count, and page metadata automatically. The controller just passes page/size parameters through. @Transactionalsemantics — annotating service methods ensures that all database operations within a method either commit together or roll back together, preventing partial updates.
Exposed the service layer as a REST API.
Key concepts:
- RESTful design — resources are nouns (
/tasks), HTTP verbs express intent (POSTto create,PUTto replace,DELETEto remove), and status codes communicate outcomes. - HTTP status codes —
201 Createdfor new resources,200 OKfor successful reads/updates,204 No Contentfor deletes,404 Not Foundwhen a resource doesn't exist,400 Bad Requestfor validation failures. @Validbean validation — placing@Validon@RequestBodyparameters triggers Jakarta Bean Validation constraints (@NotBlank,@Size,@NotNull) before the method body executes.ResponseEntity— wraps the response body with explicit control over the HTTP status code and headers, making the API contract clear in the code.
Centralised error handling with a consistent response format.
Key concepts:
@RestControllerAdvice— a single class intercepts exceptions thrown anywhere in the controller layer and maps them to structured JSON responses, avoiding duplicated try/catch blocks.- Structured error responses — the
ErrorResponserecord provides a consistent shape (timestamp,status,error,message) so API consumers can parse errors programmatically. - Validation error aggregation —
MethodArgumentNotValidExceptioncarries all constraint violations; the handler collects them into a list so the client sees every field error in one response rather than one at a time.
End-to-end integration verification of all layers working together.
Key concepts:
- Verified that the full request lifecycle (HTTP → Controller → Service → Repository → DB → back) works correctly with a real database.
- Confirmed Flyway migrations run cleanly on a fresh schema and that the API returns correct responses for all five endpoints.
Containerised the application and orchestrated it with Docker Compose.
Key concepts:
- Multi-stage Docker builds — the
builderstage (eclipse-temurin:21-jdk-alpine) compiles the JAR; theruntimestage (eclipse-temurin:21-jre-alpine) copies only the JAR, producing a smaller final image without build tools. - Docker Compose service orchestration —
docker-compose.ymldeclares both services, their environment variables, port mappings, and the dependency order (depends_onwithcondition: service_healthy). - Health checks — the
pg_isreadyhealth check on thedbservice prevents the API from starting before PostgreSQL is ready to accept connections. - Named volumes —
postgres_datapersists database files across container restarts without binding to a host path, keeping the setup portable. - Environment variable injection —
${DB_PASSWORD}indocker-compose.ymlreads from.envat runtime, keeping secrets out of version control.
Added auto-generated API documentation.
Key concepts:
- SpringDoc auto-generation —
springdoc-openapi-starter-webmvc-uiinspects Spring MVC annotations at startup and generates an OpenAPI 3 spec at/v3/api-docswith no extra configuration. @Operation/@Schemaannotations — enrich the generated spec with human-readable summaries, descriptions, and example values that appear in Swagger UI.- Swagger UI — the bundled UI at
/swagger-ui.htmllets developers explore and test every endpoint interactively without a separate tool.
Wrote the project README.
Key concepts:
- Technical writing — good documentation answers "what is this?", "how do I run it?", and "how do I use it?" in that order, matching the reader's mental journey from discovery to usage.
- README as a first impression — for a portfolio or open-source project, the README is often the first thing a recruiter or collaborator reads. A clear architecture diagram, working code snippets, and a "What I Learned" section demonstrate both technical depth and communication skills.


