# Java Services (Spring Boot)

How a Java service or library is laid out: the build, the project root, packages and modules, configuration, migrations, and consuming `platform-core`. It does not govern how the image is built or deployed, and it does not set database naming or the test framework. Forks of upstream projects (`conductor`) keep upstream's layout.

## 1. Build

- **Maven with a committed wrapper.** That means `mvnw`, `mvnw.cmd`, `.mvn/wrapper/` and `pom.xml`, plus `.mvn/settings.xml` when the build reads GitHub Packages. Every command runs through `./mvnw`, never a global `mvn`, because a global Maven drifts from the pinned one. Gradle only where the repo already builds with it. **Never** both build files in one tree.
- **Parent `spring-boot-starter-parent`.** Every third-party version is a `<properties>` entry (`<tink.version>`), referenced as `${tink.version}`, and never a literal inside a `<dependency>`. Versions Boot already manages carry no `<version>` at all. A pin that overrides one of Boot's managed versions carries its retirement condition in a comment.
- **Java version.** It lives in `<java.version>` alone and must equal the `java_version` in `nurix-svc.yml`, because CI builds with the latter. Use an LTS only (17 or 21). A new service takes 21, which runs platform-core's Java 17 bytecode unchanged.
- **The artifact** is the executable jar from `spring-boot-maven-plugin`, named by `<finalName>` without a version. One jar runs in every environment.

## 2. Project root

```
<service>/
├── mvnw  mvnw.cmd  .mvn/{wrapper/, settings.xml, maven.config}
├── pom.xml
├── compose.yaml                      # local MySQL/Redis for development only
├── src/main/java/ai/nurix/<service>/ # code, under one root package
├── src/main/resources/application.yaml
├── src/main/resources/db/migration/  # Flyway, V<n>__<snake_case>.sql
├── src/test/java/ai/nurix/<service>/
└── src/test/resources/
```

Maven's standard directory layout is fixed. Never add a second source root without the build-helper plugin, and never put resources inside `src/main/java`. `target/` is gitignored.

## 3. Packages

- **One root package, `ai.nurix.<service>`.** platform-core's reflection scans and serializers admit only `ai.nurix.*` types, so a `com.nurixlabs.*` or `ai.nuplay.*` root breaks silently when it adopts platform-core. Never use the default package.
- **The `@SpringBootApplication` class sits in the root package**, above everything it scans. Component scanning and test-context discovery both walk from it.
- **Group by capability, never by layer at the top.** This is platform-core's own shape in its newer modules: `ai.nurix.cloud.core.{storage,messaging,email}` and `ai.nurix.cloud.aws.{storage,messaging,email}`. A direct sub-package of the root is one capability, feature or surface (`storage`, `oauth`, `billing`). It holds that capability's interface, implementation, `*Properties` and `*AutoConfiguration` (in a library), or its controller, service, repository and request/response records (in a service), together. The top-level `controller/ service/ repository/ models/ utils/` split of platform-core's older `platform-common` is what grows 72-file `service` packages in consumers. Never copy it into a new package.
- **Roles appear only inside a capability**, as sub-packages named for the role, once it outgrows ~15 files or has a distinct sub-concern. platform-core's names are the vocabulary:
  - `api`: the public interfaces;
  - `config`: properties and wiring;
  - `provider`: the interchangeable implementations;
  - `support`: internal glue;
  - `listener` or `annotation`: one sub-concern each (`messaging/listener`, `email/feedback`).
- **The root of the base package** holds the application class (or a library's top-level `*AutoConfiguration` and `*Properties`) and one `exception` package for the exception types every capability throws (`ai.nurix.cloud.core.exception`).
- **Public is the API, and everything else is package-private.** In platform-cloud the implementations (`class S3StorageService`) carry no `public`. Only the interfaces and the `*AutoConfiguration` classes do, so a consumer can never import a provider type. A service applies the same rule to its repositories, mappers and helpers. Where an existing layered package forces everything public, an ArchUnit or Spring Modulith `verify()` test enforces the boundary instead.
- **Code used by several capabilities** lives in packages named for what they are (`tokens`, `identity`, `events`). **Never** a new `util`, `utils`, `common`, `helper` or `core`, because each is a membership test nobody can fail. platform-core's existing ones are legacy. App-wide wiring for a service (`WebMvcConfigurer`, filters) lives in one `config` (or `app`) package. A `@ConfigurationProperties` record sits in the package of the capability it configures.
- **Package names are singular and lowercase** (`annotation`, `exception`, `listener`, `provider`). platform-core mixes `annotation` and `annotations`, but new packages take the singular form.
- **No split packages.** A jar never declares a package that a dependency also declares (`ai.nurix.platform.*` inside a service jar).
- **Entities never cross the web boundary.** A controller takes and returns records, and never injects a repository.
- **Reference layouts:** for a library and its module split, platform-core's `platform-cloud/`. For a service, `nurixlabs/NuStack-auth`'s `broker/src/main/java` (`app/` wiring, `shared/<domain>/`, `surfaces/<surface>/`), as a reference for package shape only. Its hand-rolled router is not the style for controllers, which use `@RestController`.

## 4. Modules

- **One module by default.** A split into Maven modules needs a **second consumer**:
  - another deployable in the same repo;
  - an artifact another repo depends on (a client SDK, a shared `-domain` of wire records);
  - **interchangeable implementations behind one API.**

  A `-domain`/`-core` pair with no second consumer is ceremony. So is a module per bounded context declared before the code exists.
- **The API/implementation split is platform-cloud's shape.**
  - A nested aggregator (`platform-cloud/`) holds `-core` and one module per implementation (`-aws`, `-gcp`, `-azure`).
  - `-core` carries the interfaces, the shared types and the exceptions, with no SDK dependency. Consumers compile against `-core` only.
  - Each implementation module depends on `-core`, keeps its classes package-private, and is selected by a build profile plus a property.
- **Module names follow `<product>-<concern>[-<variant>]`** (`platform-cache`, `platform-cloud-aws`), and the folder name equals the `artifactId`. A family of modules sits under its own aggregator folder with its own `README.md`.
- **A multi-module build** uses an aggregator POM (`<packaging>pom</packaging>`) with one version, inherited by every child and declared nowhere else. In a service, exactly one module carries the Boot plugin and the main class, and the others are plain jars.
- **A shared library** built for Spring exposes its beans through auto-configuration (`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`, `@ConditionalOnMissingBean` on every bean so a consumer can override it). Consumers never `@Import` a configuration class.

## 5. Configuration

- **One `application.yaml`.** **Never** `.properties` beside it, because `.properties` silently wins. Every value that differs per environment is a `${ENV_VAR:dev-default}` placeholder, and the deploy supplies it as an environment variable. `application-{dev,stage,prod}.yaml` files hold no values. A `local` profile is the only profile file.
- **Secrets never appear in a tracked file**, including defaults. A required secret has no default, so a missing one fails the boot rather than running with a placeholder.
- **Related keys bind to a `@ConfigurationProperties` record** with `@Validated` constraints, enabled through `@ConfigurationPropertiesScan`. Avoid scattering `@Value` across beans. Custom keys never sit under `server.*`, `spring.*` or `management.*`.

## 6. Migrations

- **Flyway at `db/migration`**, one file per change, named `V<n>__<snake_case>.sql`. An applied migration is never edited. A fix is the next `V<n>`.
- **Flyway owns the schema.** Set `spring.jpa.hibernate.ddl-auto: validate` or `none`, and never ship `schema.sql`/`data.sql`. Development seed data is a separate script, never a versioned migration.

## 7. platform-core

- **Consume it at a release version through one `<platform-core.version>` property**, from the `github` repository at `https://maven.pkg.github.com/nurixlabs/platform-core`. **Never** a `-SNAPSHOT`, because it is not reproducible. Never hard-code the version per dependency: artifacts at mixed versions are how 2.x and 3.x end up in one service.
- **Take its seams, not its CRUD stack.** `platform-cloud-*` (storage, messaging, email) and `platform-cache` arrive through auto-configuration. `platform-common`'s `CommonConfigurations` installs an advice that answers every error with HTTP 200 inside a `{statusCode, statusMessage, body}` envelope. Never `@Import` it into a service that answers the standard `{status, message, data}` envelope with real status codes.
- **The cloud backend is a build profile.** The profiles are `cloud-aws` (active by default), `cloud-gcp` and `cloud-azure`, with `cloud-all` where one image must serve every cloud (mozart, cadence). `platform.cloud.provider` (`PLATFORM_CLOUD_PROVIDER`) selects the backend at runtime.

## 8. Tests

- **The test tree mirrors the main packages.** A test sits in the package of the class it tests, so it can reach package-private types.
- **Two kinds of test, one plugin.** This is platform-core's convention.
  - **Unit tests** are named `*Test`.
  - **Integration tests** are named `*IntegrationTest`. They run against a real dependency: Testcontainers wired by `@ServiceConnection`, or an emulator (`S3StorageMinioIntegrationTest`, `BlobStorageAzuriteIntegrationTest`).
  - Both match Surefire's default `**/*Test.java`, so `./mvnw verify` runs everything from a cold shell with no Failsafe block.
- **Test-only types** sit in a `fixtures` sub-package beside the tests that use them. A library that needs a Spring context keeps one `*TestApplication` at its test root (`PlatformCommonTestApplication`).
- **Slice tests** (`@WebMvcTest`, `@JdbcTest`, `@DataJpaTest`) are preferred over a full `@SpringBootTest` when one layer is the subject.

## 9. Pitfalls

- **`401 Unauthorized` resolving `ai.nurix.core:*` from `maven.pkg.github.com`.** `GITHUB_TOKEN` lacks `read:packages`, or the `<server><id>` in `.mvn/settings.xml` differs from the `<repository><id>` (both must be `github`). Locally, export a token with `read:packages`, and put `--settings=.mvn/settings.xml` in `.mvn/maven.config` so `./mvnw` reads it.
- **`No qualifying bean of type ai.nurix…` at boot.** The main class sits below the package of the missing bean, or the service's root is not under `ai.nurix`. Move the main class to the root package.
- **`Validate failed: Migrations have failed validation … checksum mismatch`.** An applied migration was edited. Restore it and add the change as the next version. Never run `flyway repair` to make the error go away.
- **Testcontainers fails with `client version 1.32 is too old` against a current Docker Engine.** Boot 3.3 manages Testcontainers 1.19. Pin `<testcontainers.version>` to 1.21.x, because 2.x renames the artifacts Boot 3.3 knows.
