Skip to content

About

Backend Modulith providing APIs for reports and integrations for the Aam Digital platform

Resources

Contributing

Security policy

Stars

23 stars

Watchers

1 watching

Forks

Repository files navigation

Aam Digital Services (aam-backend-service)

Collection of aam-digital services and tools

Maintainability Code Coverage

A modularize Spring Boot application that contains API modules for Aam Digital's case management platform.

API Modules

  • 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.

Checking availability of a feature module

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.


Setup & Configuration

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

Deployment topology

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).

Backend state lives in CouchDB

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.


Development

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).

Frameworks & Tools

Code Style

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 ktlintFormat

ktlintFormat cannot fix everything (for example lines over the 120 character limit); the rest ktlintCheck reports needs a manual edit.

Running Tests

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.

Running the e2e tests

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 test task reports UP-TO-DATE and nothing actually executes. Force a re-run by prefixing clean, e.g. ./gradlew clean test --tests "*CucumberTestRunner".

Running specific scenarios

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"

OpenAPI contract enforcement

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,notification

When 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.

Running the Application Locally

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.

Folder structure

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.

Configuration through environment variables

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).

Feature Flags

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.

Adding a flag for a new feature module

  1. 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
  2. Annotate every bean that should only exist when the module is enabled with the new annotation — @RestController, @Component, @Configuration, and any @Bean factory inside.

  3. Register a FeatureRegistrar so the module appears in /actuator/features (see ReportingFeatureInfoEndpoint for the pattern).

  4. If the module reacts to document changes, declare a DocumentChangeHandler bean in its configuration and add a nested @ConditionalOn<Module>Enabled class to ChangesConfiguration.AnyChangeConsumerEnabled, so change-detection auto-activates with it.

  5. Document the flag and any required env vars in docs/modules/<module>.md.

Asynchronous processing

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 _changes feed separately for every enabled module's DocumentChangeHandler: 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 ReportCalculationSweeper re-triggers anything left PENDING.
  • Webhook callbacks are delivered on their own bounded executor, fire-and-forget.
  • Notifications are written to the notification-outbox CouchDB database and delivered by NotificationOutboxDrainJob, through the generic OutboxDrainer that owns the retry and backoff policy (see common/outbox/README.md).

About

Backend Modulith providing APIs for reports and integrations for the Aam Digital platform

Resources

Contributing

Security policy

Stars

23 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages