System (Spring Boot)¶
Backend application for the Smartsapp platform. Modular structure under modules/ (e.g. customer/canteen); one deployable JAR.
First-time setup¶
Enable the pre-commit hook that runs tests before each commit:
git config core.hooksPath .githooks
This only needs to be done once after cloning. The hook automatically runs backend tests when backend files are staged, and skips them for non-backend changes.
Run locally (backend developers)¶
Prerequisites: Java 21+, Podman (or Docker).
1. Start infrastructure¶
From this directory, start only the infrastructure services in containers:
podman-compose up postgres kafka redis redpanda-console
2. Run the application¶
The app uses Spring Boot DevTools — it automatically restarts when classfiles on the classpath change. DevTools only reacts to .class file changes, so something has to keep recompiling your sources while bootRun is running, or your edits will never make it into the running JVM.
- With Gradle (recommended): From this directory, run
gradle wrapper(once), then:APP_SEED_DATA=true ./gradlew bootRun --continuous --continuous(a.k.a.-t) makes Gradle watch the sources and recompile on every save. DevTools sees the fresh.classfiles and triggers an in-place restart (~2s).- Plain
./gradlew bootRundoes not hot-reload. Nothing is rebuilding your classes, so DevTools has nothing to notice. The running JVM will silently drift from your source tree — you can spend hours debugging "why isn't my controller registered" when the real answer is "you haven't compiled it since startup". If you hit that, a full process restart fixes it, but--continuousprevents it in the first place. -
APP_SEED_DATA=trueloads the dev seed data on startup. Leave it off if you want to run against existing DB state. -
With IDE: Run
com.smartsapp.system.SystemApplicationas the main class. Enable auto-build in your IDE so DevTools picks up changes instantly: - IntelliJ: Preferences → Build, Execution, Deployment → Compiler → Build project automatically. Then Advanced Settings → Allow auto-make to start even if developed application is currently running. Without both of these, IntelliJ won't recompile while the app is running and you'll hit the same "stale JVM" problem as plain
bootRun.
Server starts on http://localhost:8080. Health check: GET http://localhost:8080/api/health.
Troubleshooting "my new controller returns 404": Your classes probably weren't recompiled. Confirm with
curl -s http://localhost:8080/v3/api-docs | python3 -c "import sys,json; print(len(json.load(sys.stdin)['paths']))"— if the count is way lower than expected, the running JVM is missing modules. RestartbootRunwith--continuousand rebuild.
Run locally (frontend engineers / no JDK)¶
If you just need the backend API running and don't want to install Java, run everything in containers:
podman-compose up --build
No Java installation required. The backend builds and runs inside the container. Your frontend dev server (e.g. npm run dev) can call the API at http://localhost:8080.
To run in the background: podman-compose up -d --build.
Services¶
| Service | URL / Port | Notes |
|---|---|---|
| Spring Boot API | http://localhost:8080 | |
| Postgres | localhost:5432 |
db/user/password: system |
| Kafka (Redpanda) | localhost:19092 |
Kafka-compatible broker |
| Redis | localhost:6379 |
Caching |
| Redpanda Console | http://localhost:8085 | Topic browser and message viewer |
Data is stored in named volumes postgres_data, kafka_data, and redis_data.
Build¶
./gradlew build
JAR: build/libs/system-0.0.1-SNAPSHOT.jar.
Testing¶
Integration tests use Testcontainers to run real PostgreSQL, Redis, and Kafka containers. No infrastructure mocking — tests hit the full stack.
Running tests¶
From the project root:
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home \
./backend/gradlew -p backend test
From the service directory:
cd backend
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ./gradlew test
Prerequisites¶
- Podman (or Docker) must be running — Testcontainers needs a container runtime
- Podman socket is auto-detected by
build.gradle.kts— no manualDOCKER_HOSTsetup needed - Ryuk is disabled automatically for Podman compatibility
Coverage report¶
A JaCoCo coverage report is generated automatically after every test run. The build fails if line coverage drops below 85%. Open the report in your browser:
open build/reports/jacoco/test/html/index.html
The XML report at build/reports/jacoco/test/jacocoTestReport.xml is consumed by SonarQube in CI.
Test structure¶
Tests mirror the main source layout under src/test/java/. Each module's controller tests live under modules/<module>/controllers/. See the sample module for a working reference (TodoControllerTest).
Architecture rules¶
ArchUnit tests enforce module boundaries at build time (ArchitectureTest.java). Cross-module imports are restricted:
| Import target | Allowed? |
|---|---|
Another module's services, entities, or events |
Yes |
shared (any package) |
Yes |
Another module's controllers, repositories, mappers, config, etc. |
No — build fails |
This keeps each module's internal wiring private and forces inter-module communication through service and entity APIs.
API documentation¶
The app uses springdoc-openapi to generate OpenAPI 3 specs from controller annotations. Endpoints are grouped by module.
Swagger UI (interactive): http://localhost:8080/swagger-ui.html Scalar (modern alternative): http://localhost:8080/scalar.html Use the group dropdown (top-right) to switch between modules.
Raw specs:
| Group | URL |
|---|---|
| System (health) | GET /v3/api-docs/system |
| Customer – Canteen | GET /v3/api-docs/customer-canteen |
| Customer – Student Attendance | GET /v3/api-docs/customer-attendance |
| Customer – Academic Report | GET /v3/api-docs/customer-academic-report-staff |
| Customer – Invoice | GET /v3/api-docs/customer-invoice-staff |
| Customer – School | GET /v3/api-docs/customer-school |
| Customer – Wallet | GET /v3/api-docs/customer-wallet |
| Platform – Auth | GET /v3/api-docs/platform-auth |
| Platform – Chat | GET /v3/api-docs/platform-chat |
| Platform – Engagement | GET /v3/api-docs/platform-engagement |
| Platform – Notifications | GET /v3/api-docs/platform-notifications |
Groups without controllers yet return "paths": {} — that's expected. New controllers in the right package are picked up automatically.
To add a controller to a group, place it under modules/{domain}/{module}/controllers/ and annotate it with @Tag(name = "..."). See src/main/java/com/smartsapp/system/config/OpenApiConfig.java for the package-to-group mapping.
The specs are also consumed by the developer documentation portal (Backstage) — see infrastructure/operational_utilities/developer_tools/documentation-portal/README.md.
Request headers¶
X-Request-Id¶
Every request is assigned a request ID for tracing and debugging. The client can send it; if omitted, the server generates a UUID automatically.
| Aspect | Detail |
|---|---|
| Header | X-Request-Id |
| Direction | Request and response |
| Required | No — auto-generated if missing |
| Scope | All requests |
The request ID is:
- Added to MDC for structured logging (
requestIdfield) - Echoed back in the response header
- Included in all error responses as
"requestId"in the ProblemDetail body
This allows clients to correlate a failed request with server-side logs by quoting the request ID.
Idempotency-Key¶
Mutating endpoints that create resources or trigger side effects (e.g. placing an order, confirming payment) require an idempotency key to prevent duplicate processing on network retries.
| Aspect | Detail |
|---|---|
| Header | Idempotency-Key |
| Direction | Request only |
| Required | Yes, on annotated endpoints (see below) |
| Value | Client-generated UUID (or any unique string) |
How it works:
- Client sends a unique
Idempotency-Keywith the request - Server stores the key and caches the response
- If the same key is sent again, the server returns the cached response without re-executing
- If a request with the same key is still processing, the server returns
409 Conflict
Endpoints requiring idempotency keys:
| Endpoint | Operation |
|---|---|
POST /api/canteen/parent/orders |
Place a pre-order |
PUT /api/canteen/parent/orders/batch/{batchId}/checkout |
Checkout a batch — creates an invoice via the payment-switch facade and moves the orders to PENDING_PAYMENT; the legacy invoice.paid Kafka event finalises them as PAID |
To mark additional endpoints, annotate the controller method with @RequireIdempotencyKey.
Error responses:
| Status | Condition |
|---|---|
400 |
Idempotency-Key header is missing on a required endpoint |
409 |
A request with this key is already being processed (concurrent duplicate) |
X-Trace-Id¶
Every response includes the OpenTelemetry trace ID, enabling clients to correlate a request with backend logs, spans, and Kafka message flows.
| Aspect | Detail |
|---|---|
| Header | X-Trace-Id |
| Direction | Response only |
| Value | OpenTelemetry 32-character hex trace ID |
The trace ID is automatically propagated across Kafka messages by the OTel instrumentation, so the same ID links an HTTP request to any downstream async processing.
Filter execution order¶
| Order | Filter | Purpose |
|---|---|---|
| +1 | RequestIdFilter |
Request ID → MDC + response header |
| +5 | JwtAuthFilter |
JWT auth + user context → MDC |
| +10 | AuditFilter |
Audit context |
| +15 | IdempotencyFilter |
Idempotency key check + response caching |
| lowest | TraceIdFilter |
OTel trace ID → response header |
Structure¶
src/main/java/com/smartsapp/system/
├── SystemApplication.java # Spring Boot entry point
├── config/ # App-wide config (OpenAPI, Kafka, etc.)
├── interfaces/web/ # Top-level endpoints (health checks)
├── shared/ # Cross-module utilities, base classes
└── modules/
├── sample/ # Sample Todo module (MVC reference)
├── customer/ # Customer-facing modules
│ ├── canteen/
│ ├── attendance/
│ ├── academicreport/
│ ├── invoice/
│ ├── school/
│ └── wallet/
└── platform/ # Platform / infrastructure modules
├── auth/
├── chat/
├── engagement/
├── legacymigration/
├── notifications/
└── paymentswitch/
Tests mirror this layout under src/test/java/.
Module architecture (MVC / package-by-layer)¶
Each module follows a flat MVC / package-by-layer structure:
<module>/
├── controllers/ # REST controllers (@RestController)
├── dto/ # Request/response records, event payloads
├── models/ # JPA entities (domain + persistence combined)
├── repositories/ # Spring Data JPA interfaces
├── services/ # Business logic (@Service)
├── integrations/ # Third-party adapters (only when calling external systems)
├── listeners/ # Kafka / message consumers (@KafkaListener)
└── producers/ # Kafka / message producers (@Component)
Layer responsibilities:
| Layer | What goes here | Framework annotations |
|---|---|---|
| models | JPA entities — domain objects with persistence annotations. | @Entity, @Table |
| repositories | Spring Data JPA interfaces. No implementations needed. | Extends JpaRepository |
| services | Business logic. Injects repositories, producers, integrations, and other services. | @Service |
| controllers | REST endpoints. Map DTOs, delegate to services. | @RestController, @Tag, @Operation |
| dto | Request/response records and Kafka event payloads. | @Schema (OpenAPI) |
| integrations | Third-party adapters — anything that actually talks to an external system (HTTP client, SDK, vendor stub). Only created when the module needs to call outside the app. Optionally has an integrations/dto/ subpackage for the wire-format records the integration speaks. |
@Component / @Service |
| listeners | Kafka consumers. Delegate to services. | @Component, @KafkaListener |
| producers | Kafka producers. Wrap KafkaTemplate. |
@Component |
Dependency flow:
controllers ─→ services ─→ repositories
│ ↓ ↓
│ producers models
↓
integrations ─→ external systems
↑
listeners ─→ services
Controllers and listeners are entry points; services orchestrate business logic; repositories and producers handle persistence and messaging; integrations encapsulate every call out to a third party.
Naming conventions¶
| Layer | Pattern | Example |
|---|---|---|
| Models (JPA entities) | ClassName |
Canteen, Item |
| Repositories | *Repository |
CanteenRepository |
| Services | *Service |
CanteenService |
| Controllers | *Controller |
CanteenController |
| Request DTOs | Create*Request / Update*Request |
CreateCanteenRequest |
| Response DTOs | *Response |
CanteenResponse |
| Event DTOs | *Event |
CanteenEvent |
| Producers | *EventProducer |
CanteenEventProducer |
| Listeners | *EventListener |
CanteenEventListener |
| Integrations | *Integration |
LegacyApiIntegration, StubPaymentSwitchIntegration |
Package names cannot contain hyphens: academic-report → academicreport.
Adding a new module¶
- Create the directory tree under
modules/customer/<name>/(orplatform/<name>/) with subpackages:controllers,dto,models,repositories,services,listeners,producers. Add anintegrations/subpackage only if the module needs to call third-party systems. - Add a
GroupedOpenApibean inOpenApiConfig.javascanning...modules.customer.<name>.controllers. - Create the matching test directory under
src/test/java/.
See the modules/sample/ module for a complete working reference with CRUD endpoints, Kafka producer, and Kafka listener.