lifeos handbook
Concepts

The monorepo

Nx above Gradle and Bun, and who actually owns each job.

Nx compiles nothing and installs nothing. It sits above the real tools: it infers the dependency graph, decides what a change affected, caches results, and runs tasks in order (ADR-0002).

JobOwnerNx's role
Compile Java, run JUnit + ArchUnitGradle (@nx/gradle)call it, cache it, run when affected
Install JS deps, pin versionsBun (workspaces + root catalog:)none — Bun owns versions
Bundle web (Vite), run VitestBuncall it, cache, scope to affected
Format + lint frontendBiomenone — Biome owns it
Enforce backend layeringGradle deps + ArchUnitnone
Decide what to build, and deploy itNxthe whole job

Convention plugins keep build files short

Shared Gradle configuration lives in build-logic/lifeos.java-base (toolchain 26, the Spring Boot BOM, Lombok and MapStruct wiring, JUnit Platform), lifeos.java-library, and lifeos.spring-boot-app. That is why an app's build.gradle.kts is about ten lines.

The graph assertion, and why it exists

The root build.gradle.kts applies dev.nx.gradle.project-graph to allprojects, which is what puts Gradle modules into the Nx graph. With a mismatched plugin version they disappear from the graph entirelynx show projects returns only the JavaScript projects, nx affected stops seeing any backend, and a deploy job goes green having deployed nothing.

An empty affected set and a broken graph must not look the same, so CI asserts the Gradle projects are present and fails loudly if they are not.

A module owns its repositories

Only classes in the same module inject that module's repositories. To reach another domain's data, go through that domain's service — its public API — never its repository directly. Cross-module repository access couples domains and bypasses the owning service's rules.

This is enforced by an ArchUnit rule in a shared test library, not by good intentions. A rule enforced in one app is not a convention.

Watch for the bean cycle this creates. A module's CRUD service (which the ledger depends on) has to be a separate bean from its money-reading service (which depends on the ledger) — hence ProjectService/ProjectFinancialsService and AccountService/OwedService. Tidying those together reintroduces the cycle.

On this page