Backend Overview
Lumie's backend is one Spring Boot deployable assembled from app, libs/*,
and the Gradle subprojects included in
lumie-backend/settings.gradle.kts. The entrypoint is
lumie-backend/app/src/main/java/com/lumie/app/LumieApplication.java, which
starts a single JVM with @SpringBootApplication(scanBasePackages = "com.lumie")
and @EnableAsync.
This page is an overview document. It maps the deployable boundary, the major
runtime flows, and the shared rules that every backend module lives under.
Source Paths
| Path | Role |
|---|---|
lumie-backend/settings.gradle.kts | Declares the subprojects that ship in the backend runtime |
lumie-backend/app/build.gradle.kts | Assembles the deployable jar from app, libs/*, and included modules/* |
lumie-backend/app/src/main/java/com/lumie/app/LumieApplication.java | Spring Boot entrypoint |
lumie-backend/app/src/main/resources/application.yaml | Runtime configuration for data sources, Flyway, Modulith, RabbitMQ, Redis, MinIO, CORS, rate limits, and worker URLs |
lumie-backend/libs/common | Shared tenant, auth, exception, idempotency, logging, and base-entity utilities |
lumie-backend/libs/internal-api | In-process module contracts and cross-module events |
lumie-backend/libs/messaging | RabbitMQ queue, exchange, and routing-key constants |
lumie-backend/modules/* | Product and platform modules loaded into the same JVM |
Deployable Shape
What Ships In Process
app/build.gradle.kts wires these categories into the backend jar:
- Bootstrap runtime:
app - Shared libraries:
libs:common,libs:internal-api,libs:messaging - Platform modules: Tenant Service, Auth Service, Billing Service, Homepage Service, AI Service, Notification Service, File Service
- Education modules: Student Service, Staff Service, Class Service, Attendance Service, Assignment Service, Lecture Service, Content Service, Exam Service, Tuition Service
modules/activity-log exists in the repository tree but is not included in
settings.gradle.kts, so it is not part of the current runtime.
Primary Runtime Flows
Authenticated product request
- A controller under
modules/*/adapter/in/webreceives/v1/**. JwtAuthenticationFilteraccepts eitherAuthorization: Bearer ...or thelumie_access_tokencookie.- Tenant and user context are written into
TenantContextHolderandUserContextHolder. - The owning application service executes inside a transaction.
RlsTenantContextAspectbindsapp.tenant_idso PostgreSQL RLS can filter tenant-scoped rows.- The module either commits synchronously or emits a post-commit event for follow-up work.
Internal worker or platform request
/internal/**requests are authenticated byInternalHmacAuthFilter, not by end-user JWT.- The filter verifies
X-Tenant-Slug,X-Timestamp, andX-Signature, resolves the tenant ID, and grants syntheticROLE_INTERNAL. - Downstream code runs with the same tenant context and RLS enforcement as a normal request.
After-commit async follow-up
- A module publishes a Spring Modulith event inside the same database transaction as its domain write.
- The event is stored in
public.event_publication. - After commit, an
@ApplicationModuleListenerhandles the event in-process or forwards it to RabbitMQ. - If the listener or broker send fails, the publication remains incomplete and
is retried on restart because
spring.modulith.events.republish-outstanding-events-on-restart=true.
Shared Boundary Rules
- The backend is a modular monolith, not a set of separately deployed Java services. Module names describe ownership boundaries inside one process.
- Module structure follows
domain,application, andadapter; see Architecture. - Synchronous module-to-module calls go through
libs/internal-apiinterfaces, usually implemented underadapter/in/internal/. - Durable internal events go through Spring Modulith's JDBC outbox, not direct
@TransactionalEventListenerwiring. - External Python workers are integrations. They do not own tenant data tables; the monolith remains the source of truth for transactional writes and tenant-safe reads.
Failure Modes To Keep In Mind
- Missing tenant ID in context means RLS-backed tables appear empty even if the slug is present.
- Giving the runtime database role
SUPERUSERorBYPASSRLSwould silently defeat tenant isolation, so startup is blocked byRuntimeDbRoleGuard. - Async code that loses
TenantContextHolderorUserContextHolderwill fail or read the wrong scope unless it uses the task-decorator or explicitwithinContext(...)restoration. - Queue-backed jobs are durable only when the module publishes through the outbox path; direct HTTP calls to workers do not get that durability.
Verification Commands
cd /Users/bluemayne/Projects/Lumie/lumie-backend
./gradlew test
./gradlew integrationTest
./gradlew :app:test
./gradlew :libs:common:test
Useful integration tests for this overview:
app/src/test/java/com/lumie/app/config/RoutingDataSourceIntegrationTest.javaapp/src/test/java/com/lumie/app/migration/MigrationsRlsIntegrationTest.javalibs/common/src/test/java/com/lumie/common/tenant/RlsTenantContextAspectIntegrationTest.java