Skip to content

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 .class files and triggers an in-place restart (~2s).
  • Plain ./gradlew bootRun does 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 --continuous prevents it in the first place.
  • APP_SEED_DATA=true loads the dev seed data on startup. Leave it off if you want to run against existing DB state.

  • With IDE: Run com.smartsapp.system.SystemApplication as 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. Restart bootRun with --continuous and 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 manual DOCKER_HOST setup 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 (requestId field)
  • 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:

  1. Client sends a unique Idempotency-Key with the request
  2. Server stores the key and caches the response
  3. If the same key is sent again, the server returns the cached response without re-executing
  4. 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-reportacademicreport.

Adding a new module

  1. Create the directory tree under modules/customer/<name>/ (or platform/<name>/) with subpackages: controllers, dto, models, repositories, services, listeners, producers. Add an integrations/ subpackage only if the module needs to call third-party systems.
  2. Add a GroupedOpenApi bean in OpenApiConfig.java scanning ...modules.customer.<name>.controllers.
  3. 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.