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).
| Job | Owner | Nx's role |
|---|---|---|
| Compile Java, run JUnit + ArchUnit | Gradle (@nx/gradle) | call it, cache it, run when affected |
| Install JS deps, pin versions | Bun (workspaces + root catalog:) | none — Bun owns versions |
| Bundle web (Vite), run Vitest | Bun | call it, cache, scope to affected |
| Format + lint frontend | Biome | none — Biome owns it |
| Enforce backend layering | Gradle deps + ArchUnit | none |
| Decide what to build, and deploy it | Nx | the 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 entirely — nx 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.