Team-based project management API — think Linear/Jira core, built to production-grade engineering standards.
Portfolio project demonstrating full-stack engineering: Hexagonal Architecture, stateless JWT auth, real-integration tests, structured logging, and an Angular 21 frontend with Kanban board.
Users belong to Teams. Teams own Projects. Projects contain Tasks with a state machine. Tasks have Comments, Labels, and a full Audit Trail.
User ──< TeamMembership >── Team ──< Project ──< Task
├── Comments
├── Labels
└── AuditLog (async events)
Task lifecycle:
BACKLOG → TODO → IN_PROGRESS → IN_REVIEW → DONE
↑__________↓ ↑_________↓
(backwards allowed within active states)
↘ CANCELLED (any active state)
| Java 21 + Spring Boot 3.3.4 | Runtime + framework |
| Spring Security 6 | Stateless JWT auth filter chain |
| Spring Data JPA + Hibernate 6 | ORM with explicit entity/domain separation |
| PostgreSQL 16 | Primary database |
| Flyway 10 | Schema versioning (ddl-auto: validate) |
| JJWT 0.12 | JWT generation + validation |
| SpringDoc OpenAPI 2 | Auto-generated Swagger UI |
| Testcontainers 1.20 | Real Postgres in integration tests |
| Apache HttpClient 5 | Test HTTP client (handles 401 cleanly) |
| Logstash Logback Encoder | Structured JSON logging |
| Micrometer + Prometheus | Metrics |
| Checkstyle + SpotBugs | Code quality enforcement |
| Angular 21 | SPA framework (standalone components, signals) |
| Angular Material 21 | UI component library (M3 theme) |
| Angular CDK (drag-and-drop) | Kanban board DnD |
| TypeScript 5.9 (strict) | No any — ever |
| Vitest + Angular Testing Library | Unit + component tests (124 tests) |
| Docker + Docker Compose | Full local stack |
| GitHub Actions | CI (test + lint + coverage) |
| k6 | Performance baseline scripts |
| Prometheus + pgAdmin | Monitoring + DB inspection |
Hexagonal (Ports & Adapters) — dependencies point inward only:
infrastructure/ ← Spring, JPA, HTTP, Security
└── application/ ← Use cases, DTOs
└── domain/ ← Business rules, zero framework imports
domain/— Pure Java.Task,User,Team, domain events, repository interfaces. No Spring annotations.application/— Use cases (CreateTaskUseCase,LoginUseCase, …). One class per operation.@Transactionallives here.infrastructure/— JPA entities, REST controllers, JWT filter, Flyway migrations, adapters.
Architecture Decision Records: docs/adr/
taskflow/
├── backend/
│ ├── src/main/java/dev/taskflow/
│ │ ├── domain/
│ │ │ ├── model/ Task, User, Team, Project, Comment, Label…
│ │ │ ├── exception/ DomainException, EntityNotFoundException…
│ │ │ ├── event/ TaskStatusChangedEvent, TaskAssignedEvent…
│ │ │ └── port/repository/ TaskRepository, UserRepository… (interfaces)
│ │ ├── application/
│ │ │ ├── usecase/ CreateTaskUseCase, LoginUseCase… (one class per op)
│ │ │ └── dto/ Request/Response records
│ │ └── infrastructure/
│ │ ├── persistence/ JPA entities, adapters, mappers, Spring Data repos
│ │ ├── rest/ Controllers, GlobalExceptionHandler
│ │ ├── security/ JwtAuthenticationFilter, JwtService, SecurityConfig
│ │ └── config/ JwtProperties, OpenApiConfig, CorrelationIdFilter, TaskMetrics
│ ├── src/main/resources/
│ │ ├── db/migration/ V1__init_schema.sql (Flyway)
│ │ └── application.yml + application-dev.yml
│ ├── src/test/
│ │ ├── java/.../integration/ AbstractIntegrationTest + per-feature tests
│ │ └── resources/application-test.yml
│ ├── build.gradle.kts
│ └── Dockerfile Multi-stage (eclipse-temurin:21-alpine)
│
├── frontend/ Angular 21 (Kanban board, signals, 124 tests)
│ └── Dockerfile Multi-stage (nginx)
│
├── infra/
│ ├── docker-compose.yml postgres, pgadmin, backend, frontend, prometheus
│ ├── prometheus/prometheus.yml
│ └── k6/baseline.js Performance baseline (10 VUs, p95<500ms threshold)
│
├── docs/
│ ├── adr/ ADR-001, ADR-002, ADR-003
│ ├── taskflow-postman-collection.json
│ └── api-curl-cheatsheet.md
│
├── lessons/ Living technical documentation
│ ├── README.md Index of all lessons
│ └── 01…10-*.md
│
├── CLAUDE.md AI assistant project instructions
└── PLAN.md Full engineering spec + phase tracker
- Java 21 (SDKMAN:
sdk install java 21.0.7-zulu) - Docker + Docker Compose (or OrbStack on macOS)
- Node 22+ (frontend only)
1. Start the database:
cd infra
docker compose up -d postgres2. Run the backend:
cd backend
JWT_SECRET=dev_secret_minimum_32_characters_required \
./gradlew bootRun --args='--spring.profiles.active=dev'Server starts at http://localhost:8080.
3. Verify:
curl http://localhost:8080/actuator/health
# {"status":"UP","groups":["liveness","readiness"]}cd infra
JWT_SECRET=dev_secret_minimum_32_characters_required \
docker compose up -d postgres backendFrontend (when ready):
docker compose up -d # includes frontend on :4200Monitoring stack:
docker compose --profile monitoring up -d| Service | URL |
|---|---|
| Backend API | http://localhost:8080 |
| Swagger UI | http://localhost:8080/swagger-ui/index.html |
| pgAdmin | http://localhost:5050 (admin@taskflow.dev / admin) |
| Prometheus | http://localhost:9090 (with monitoring profile) |
cd backend
./gradlew testRequires Docker (Testcontainers starts a real Postgres 16 container automatically).
./gradlew test --tests "dev.taskflow.integration.TaskControllerIntegrationTest"./gradlew test --tests "dev.taskflow.integration.TeamControllerIntegrationTest\$CreateTeam.createsTeamAndOwnerBecomesOwnerMember"./gradlew test jacocoTestReport
# Report: backend/build/reports/jacoco/test/html/index.html./gradlew checkstyleMain # Google style
./gradlew spotbugsMain # Static analysis
./gradlew check # Everything (test + checkstyle + spotbugs + coverage)Import docs/taskflow-postman-collection.json into Postman.
- Pre-configured collection variables:
baseUrl,accessToken,teamId,projectId,taskId, etc. - Test scripts auto-populate variables on Register/Login/Create responses — just hit Register and every subsequent request is ready.
See docs/api-curl-cheatsheet.md for copy-paste cURL commands for all 25 endpoints.
http://localhost:8080/swagger-ui/index.html — interactive docs with Bearer auth.
Auth
POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/auth/refresh
POST /api/v1/auth/logout
Teams
POST /api/v1/teams
GET /api/v1/teams/{teamId}/members
POST /api/v1/teams/{teamId}/members
DELETE /api/v1/teams/{teamId}/members/{userId}
Projects
POST /api/v1/teams/{teamId}/projects
GET /api/v1/teams/{teamId}/projects ?page=0&size=20
PATCH /api/v1/projects/{projectId}/archive
POST /api/v1/projects/{projectId}/labels
GET /api/v1/projects/{projectId}/labels
Tasks
POST /api/v1/projects/{projectId}/tasks
GET /api/v1/projects/{projectId}/tasks ?status=TODO&page=0&size=20
PUT /api/v1/tasks/{taskId}
PATCH /api/v1/tasks/{taskId}/status
PATCH /api/v1/tasks/{taskId}/assignee
POST /api/v1/tasks/{taskId}/labels/{labelId}
DELETE /api/v1/tasks/{taskId}/labels/{labelId}
DELETE /api/v1/tasks/{taskId}
Comments
POST /api/v1/tasks/{taskId}/comments
GET /api/v1/tasks/{taskId}/comments ?page=0&size=20
PATCH /api/v1/comments/{commentId}
DELETE /api/v1/comments/{commentId}
All list endpoints return:
{
"data": [...],
"pagination": { "page": 0, "size": 20, "totalElements": 47, "totalPages": 3 }
}All errors return:
{ "code": "TASK_NOT_FOUND", "message": "task with id … does not exist" }| Mechanism | Detail |
|---|---|
| Access token | JWT, HMAC-SHA256, 15-minute expiry |
| Refresh token | Opaque UUID, 7-day expiry, SHA-256 hashed in DB |
| Token rotation | Old refresh token deleted on each use |
| Password hashing | BCrypt, cost factor 12 |
| Session | Stateless — no server-side sessions, no cookies |
| Logout | Deletes all refresh tokens for the user |
| Unauthenticated requests | 401 (not 403) |
| User enumeration | Wrong email and wrong password both return 401 with identical message |
See Lesson 10 and ADR-002 for the full rationale.
12 tables managed exclusively by Flyway — ddl-auto: validate.
users id, email, password_hash, full_name, deleted_at
teams id, name, slug (unique), owner_id, deleted_at
team_memberships team_id + user_id (PK), role (OWNER|MEMBER|VIEWER)
projects id, team_id, name, status (ACTIVE|ARCHIVED), deleted_at
tasks id, project_id, title, status, priority, assignee_id, due_date, deleted_at
task_labels task_id + label_id (composite PK)
labels id, project_id, name, color (#RRGGBB)
comments id, task_id, author_id, body, edited_at, deleted_at
audit_logs id, entity_type, entity_id, action, actor_id, payload (JSONB)
refresh_tokens id, user_id, token_hash (SHA-256), expires_at, revoked_at
All timestamps: TIMESTAMPTZ stored in UTC. All soft-deletes via deleted_at IS NULL filter.
See backend/src/main/resources/db/migration/V1__init_schema.sql.
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SECRET |
Yes | — | Min 32 chars. Use a random secret in production. |
SPRING_DATASOURCE_URL |
No | jdbc:postgresql://localhost:5432/taskflow |
Postgres JDBC URL |
SPRING_DATASOURCE_USERNAME |
No | taskflow |
|
SPRING_DATASOURCE_PASSWORD |
No | taskflow_dev |
|
JWT_ACCESS_EXPIRATION_MS |
No | 900000 (15 min) |
Access token TTL in ms |
JWT_REFRESH_EXPIRATION_MS |
No | 604800000 (7 days) |
Refresh token TTL in ms |
| Phase | Status | Notes |
|---|---|---|
| 1 — Foundation | ✅ Done | Java 21, Gradle, Docker, Flyway, CI, ADRs |
| 2 — Domain + Auth | ✅ Done | JWT auth cycle, 37 unit tests, integration tests |
| 3 — Core API | ✅ Done | 72/72 integration tests passing, 25 endpoints |
| 4 — Observability | ✅ Done | Micrometer gauge, MDC correlation ID, structured logging, k6 baseline, Checkstyle+SpotBugs clean |
| 5 — Frontend | ✅ Done | Angular 21, Kanban board, JWT interceptor, 124 tests |
| 6 — Polish | ⬜ Planned | Final k6 run, README diagrams |
| Document | Description |
|---|---|
lessons/ |
Living technical documentation — architecture, bugs fixed, decisions explained |
docs/adr/ |
Architecture Decision Records (hexagonal arch, JWT strategy, Flyway) |
docs/api-curl-cheatsheet.md |
cURL commands for all 25 endpoints |
docs/taskflow-postman-collection.json |
Importable Postman collection with auto-token scripts |
infra/k6/baseline.js |
k6 load test — 10 VUs, full CRUD flow, p95 < 500ms threshold |
PLAN.md |
Full engineering spec, standards, and phase tracker |
- No Lombok — explicit getters, constructors, builders. Readable without IDE plugins.
- Conventional Commits —
feat:,fix:,test:,chore:,docs: - No
@Componenton filters registered insideSecurityFilterChain— causes double-registration. See Lesson 05. @Transactionalon use cases only — not on domain, not on controllers.EnumType.STRINGalways — neverORDINAL../gradlew checkmust pass before any commit.