Collection of aam-digital services and tools
A modularize Spring Boot application that contains API modules for Aam Digital's case management platform.
- Reporting: Calculate aggregated reports and run queries on all data, accessible for external services for API integrations of systems
- Export: Template based file export API. Uses carbone.io as templating engine.
- Skill: Integration with external system (SkillLab) to link and pull data into Aam Digital.
- Notification: Push Notification system, triggering events based on custom rules for each user.
- Third-Party-Authentication: "Single-Sign-On" integration with other platforms.
- Admin: Administration by the app's administrators, e.g. self-service API clients (the credentials of external systems for the other modules' APIs).
Modules have to be enabled via a feature flag in the environment config and may need additional environment variables as described in their module docs.
You can make a request to the API to check if a certain feature is currently enabled and available:
> GET /actuator/features
// response:
{
"reporting": { "enabled": true },
"notification": { "enabled": true },
"export": { "enabled": true },
"skill": { "enabled": true },
"third-party-authentication": { "enabled": true },
"admin": { "enabled": true, "api-client": { "enabled": true } }
}
The response lists feature modules with their status ("enabled"). If the aam-services backend is not deployed at all, such a request will usually return a HTTP 504 error. You should also account for that possibility.
This API is part of the larger Aam Digital platform and usually deployed together with additional services via docker. See the ndb-setup repository for tools and guidance how to run this in a production environment.
For instructions to enable the backend in an overall system: ndb-setup README
In production each Aam Digital instance runs its own data stack: its own CouchDB container alongside this service, as defined in ndb-setup's docker-compose.yml. None of it is shared between instances.
What instances do share is the layer underneath and in front of them — the host they run on, the
external_web Docker network (declared external: true, created outside the instance stack).
The state this service keeps between restarts lives in CouchDB:
| what | where it lives |
|---|---|
| change-detection cursors | aam-backend-state, SyncEntry:<database>:<consumer>, one per module |
| push device registrations | aam-backend-state, UserDevice:<deviceToken> |
| third-party-auth redirect bindings | aam-backend-state, ThirdPartyAuthSession:<sessionId> |
| third-party-auth login tickets | nowhere — in memory, they expire within minutes |
| SkillLab profile mirror | skill-user-profile, SkillProfile:<externalId> |
| SkillLab sync cursor | skill-user-profile, SkillLabUserProfileSync:<projectId> |
Neither database is in database-change-detection.included-databases.
replication-backend does not block these databases, so users whose permission rules grant broad access
(e.g. manage all) can read them, which is accepted.
The individual modules like "Reporting" require some setup and environment variables. Please refer to the respective READMEs in the "API Modules" list above for instructions about each API Module.
To run the system locally for development, refer to the docs/developer/README.md and sample docker files there. These allow you to run required additional services like databases and Keycloak on your machine.
The developer README provides detailed instructions how to set up a local testing environment: docs/developer/README.md
This backend is developed as independent modules that share some common services (e.g. for database access).
Formatting is checked with ktlint, configured in
application/aam-backend-service/.editorconfig. CI runs ktlintCheck and fails on any finding.
Run these from application/aam-backend-service/:
# Check formatting (what CI runs)
./gradlew ktlintCheck
# Fix what can be fixed automatically
./gradlew ktlintFormatktlintFormat cannot fix everything (for example lines over the 120 character limit); the rest
ktlintCheck reports needs a manual edit.
All commands should be run from application/aam-backend-service/.
# Run all tests (unit + e2e)
./gradlew test
# Run tests with coverage report (CSV + XML in build/reports/jacoco/)
./gradlew jacocoTestReport
# Run a single test class
./gradlew test --tests "com.aamdigital.aambackendservice.export.usecase.DefaultCreateTemplateUseCaseTest"The test suite includes:
- Unit tests (JUnit Jupiter + Mockito) for individual use cases and services
- E2E / integration tests (Cucumber BDD) that spin up real Docker containers via Testcontainers (Keycloak, CouchDB, Carbone, SQS) and test full API flows. Cucumber feature files are located in
src/test/resources/cucumber/features/.
Both run together with ./gradlew test.
Prerequisites: JDK 25 and a running Docker daemon — Testcontainers starts and tears down all the containers itself, so no other local setup (no docker-compose, no manual Keycloak) is needed.
Run only the e2e tests (skips the unit tests):
./gradlew test --tests "*CucumberTestRunner"Booting the full container stack and Spring Boot takes ~1–2 minutes before the first scenario runs, regardless of how many scenarios you select.
Gradle caches passing tests. If you re-run without changing any source, the
testtask reportsUP-TO-DATEand nothing actually executes. Force a re-run by prefixingclean, e.g../gradlew clean test --tests "*CucumberTestRunner".
Filter scenarios with environment variables (always keep --tests "*CucumberTestRunner"
so the unit tests stay skipped):
# By tag (add e.g. @Focus to the scenario in the .feature file)
CUCUMBER_FILTER_TAGS="@Focus" ./gradlew test --tests "*CucumberTestRunner"
# By an existing tag (e.g. @Notification)
CUCUMBER_FILTER_TAGS="@Notification" ./gradlew test --tests "*CucumberTestRunner"
# By scenario name (regex)
CUCUMBER_FILTER_NAME="client makes call to start a report calculation" ./gradlew test --tests "*CucumberTestRunner"
# By feature file path (optionally with :line for a single scenario)
CUCUMBER_FEATURES="src/test/resources/cucumber/features/notification/notification-change-type.feature:8" ./gradlew test --tests "*CucumberTestRunner"The e2e suite also validates the API against the hand-written OpenAPI specs in
docs/api-specs/, so the specs stay honest (see
src/test/kotlin/.../e2e/contract/). For every call, the request/response is
checked against the owning module's spec; at the end of the run, each strict
module is checked for coverage (every documented operation is exercised) and
inventory (every controller endpoint is documented).
Strictness is per module via a system property (default: reporting,export,notification) —
unlisted modules run in report-only mode (mismatches are logged, the build stays
green). Override the default to widen, narrow, or disable enforcement:
# report-only for every module:
./gradlew test --tests "*CucumberTestRunner" -Dcontract.strict.modules=
# a custom strict set:
./gradlew test --tests "*CucumberTestRunner" -Dcontract.strict.modules=reporting,export,notificationWhen you change a module's API, update its docs/api-specs/<module>-api-v1.yaml
spec (and docs/modules/<module>.md) in the same change.
VS Code: Install the Gradle for Java extension to discover and run tests from the Test Explorer sidebar.
Prerequisites: JDK 25 and a running local dev stack (CouchDB, Keycloak, SQS). See docs/developer/README.md for full environment setup instructions.
# From application/aam-backend-service/
./gradlew bootRun --args='--spring.profiles.active=local-development'The application starts on port 9000 with context path / when using the local-development profile (i.e. http://localhost:9000/). Other profiles may configure a different context path (for example /api).
Make sure to copy the reverse-proxy certificate for HTTPS connections to Keycloak (see developer README).
VS Code: Install the Spring Boot Dashboard extension, then set the environment variable SPRING_PROFILES_ACTIVE=local-development in your launch configuration.
Each feature module is separated into its own package.
Additionally, there is a common package that contains shared code.
This includes services to work with the CouchDB, get a feed of DB changes and other common functionality.
The "common" package also holds base functionality for authentication and security.
Using Spring Boot's system our configurable values are represented in the application.yaml.
We define environment variables to set / overwrite the configuration to real values for production or testing. Variables in the yaml hierarchy can be set in env variables using "snake case" variable names (refer to the documentation of Spring Boot's Externalized Configuration docs).
Modules (usually) have to be explicitly enabled through a feature flag configuration.
For example, FEATURES_EXPORTAPI_ENABLED=true in .env toggles the export module.
Each module defines a dedicated meta-annotation at its package root that gates
all of its beans (controllers, @Configuration classes, @Bean factories, etc.) —
look for ConditionalOn<Module>Enabled.kt alongside the module's FeatureInfoEndpoint.
Use that annotation on any class or @Bean method that should only exist when the
module is enabled.
Some modules additionally expose a mode-specific meta-annotation (e.g. for picking
between alternative backends). Stack the mode annotation on top of the
Enabled annotation when a bean requires both:
@RestController
@ConditionalOn<Module>Enabled
@ConditionalOn<Module><Mode>Mode
class SomeController(...)The exact flags and modes available for each module are documented in the respective API Module docs.
-
Define the meta-annotation at the module package root (next to the module's
FeatureInfoEndpoint), e.g.application/aam-backend-service/src/main/kotlin/com/aamdigital/aambackendservice/<module>/ConditionalOn<Module>Enabled.kt:@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) @ConditionalOnProperty( prefix = "features.<module>", name = ["enabled"], havingValue = "true", matchIfMissing = false, ) annotation class ConditionalOn<Module>Enabled
-
Annotate every bean that should only exist when the module is enabled with the new annotation —
@RestController,@Component,@Configuration, and any@Beanfactory inside. -
Register a
FeatureRegistrarso the module appears in/actuator/features(see ReportingFeatureInfoEndpoint for the pattern). -
If the module reacts to document changes, declare a
DocumentChangeHandlerbean in its configuration and add a nested@ConditionalOn<Module>Enabledclass toChangesConfiguration.AnyChangeConsumerEnabled, so change-detection auto-activates with it. -
Document the flag and any required env vars in
docs/modules/<module>.md.
Work that must not block its caller is handed to a bounded executor, and work that must not be lost is recorded durably first and picked up by a scheduled job.
- Document changes are detected by polling CouchDB's
_changesfeed separately for every enabled module'sDocumentChangeHandler: each has its own cursor and its own polling thread, so a module that is slow or stuck holds back only itself. A handler runs synchronously on that thread with its cursor advanced per change, so it must still keep its inline work in memory. - Report calculations run on a bounded executor, so a handful of multi-second SQS queries can be
in flight without overwhelming SQS. The calculation document is written before the executor is
asked to run it, and
ReportCalculationSweeperre-triggers anything leftPENDING. - Webhook callbacks are delivered on their own bounded executor, fire-and-forget.
- Notifications are written to the
notification-outboxCouchDB database and delivered byNotificationOutboxDrainJob, through the genericOutboxDrainerthat owns the retry and backoff policy (seecommon/outbox/README.md).