A Java 21 and Spring Boot 3.5 service that exposes one REST API over PostgreSQL and MongoDB user data. Reads query both stores concurrently; writes go to PostgreSQL.
This is a portfolio service with production-oriented failure, validation, migration, caching, and observability patterns. It is not deployed as a production system.
- Concurrent PostgreSQL and MongoDB aggregation on a dedicated bounded executor
- A provenance-aware, paginated v2 API with deterministic cross-source ordering
- Deterministic
503 Service Unavailableresponses when either source fails or times out - JDBC query, connection-pool, and MongoDB driver timeouts derived from bounded configuration
- Per-source query duration metrics tagged by source and outcome
- Caffeine query caching with write-triggered invalidation
- Request validation and explicit
201 Created,400,409, and503API contracts - Flyway-managed relational schema migrations
- OpenAPI, Actuator health/metrics, Docker Compose, CI, CodeQL, and attested releases
- Docker Engine with Docker Compose v2
- Two local-only passwords for PostgreSQL and MongoDB
git clone https://github.com/lMysticl/user-aggregation-service.git
cd user-aggregation-service
cp .env.example .envPowerShell:
git clone https://github.com/lMysticl/user-aggregation-service.git
Set-Location user-aggregation-service
Copy-Item .env.example .envSet POSTGRES_PASSWORD and MONGO_INITDB_ROOT_PASSWORD in .env, then start the stack:
docker compose up --buildThe Compose stack binds its ports to 127.0.0.1:
| Interface | URL |
|---|---|
| REST API | http://127.0.0.1:8080/api/users |
| Paginated REST API | http://127.0.0.1:8080/api/v2/users |
| Swagger UI | http://127.0.0.1:8080/swagger-ui/index.html |
| OpenAPI document | http://127.0.0.1:8080/v3/api-docs |
| Health | http://127.0.0.1:8080/actuator/health |
The demo profile adds sample records only when a store is empty. It never clears existing data.
docker compose downTo intentionally remove the local database volumes:
docker compose down --volumes| Dependency | Version | Notes |
|---|---|---|
| JDK | 21 | The Maven compiler release and CI runtime |
| Docker | Current Docker Engine | Runs the real PostgreSQL and MongoDB contract test |
| MongoDB | 6 or newer | Required at localhost:27017 by the lightweight integration tests |
| Maven | Wrapper-provided | A separate Maven installation is not required |
Start a disposable MongoDB for local tests:
docker run --rm --name aggregation-service-test-mongo -p 127.0.0.1:27017:27017 mongo:6-jammyRun the complete quality gate:
./mvnw --batch-mode --no-transfer-progress clean verifyPowerShell:
.\mvnw.cmd --batch-mode --no-transfer-progress clean verifyGitHub Actions runs the same command on Java 21 with a MongoDB service and separately verifies that the Docker image builds. It also fails if the Testcontainers contract test against real PostgreSQL and MongoDB is skipped.
Tagged releases publish the executable JAR, SHA-256 checksum, CycloneDX SBOM, and GitHub build-provenance attestation.
With MongoDB available locally:
./mvnw spring-boot:run -Dspring-boot.run.profiles=demoPowerShell:
.\mvnw.cmd spring-boot:run "-Dspring-boot.run.profiles=demo"The default relational database is in-memory H2. Omit the demo profile to start without sample records.
| Variable | Default | Purpose |
|---|---|---|
AGGREGATION_PRIMARY_JDBC_URL |
jdbc:h2:mem:users;DB_CLOSE_DELAY=-1 |
Relational JDBC URL |
AGGREGATION_PRIMARY_JDBC_USERNAME |
sa |
Relational username |
AGGREGATION_PRIMARY_JDBC_PASSWORD |
empty | Relational password |
AGGREGATION_MONGODB_URI |
mongodb://localhost:27017/users |
MongoDB connection string |
AGGREGATION_QUERY_TIMEOUT |
2s |
Maximum wait for each source query |
AGGREGATION_CONNECTION_TIMEOUT_MS |
2000 |
Maximum wait for a pooled JDBC connection |
AGGREGATION_EXECUTOR_CORE_POOL_SIZE |
4 |
Core aggregation worker count |
AGGREGATION_EXECUTOR_MAX_POOL_SIZE |
8 |
Maximum aggregation worker count |
AGGREGATION_EXECUTOR_QUEUE_CAPACITY |
100 |
Bounded pending-query capacity |
Docker Compose reads database credentials from an ignored .env file. Use a platform secret manager outside local development.
| Method | Path | Success | Behavior |
|---|---|---|---|
GET |
/api/users |
200 |
Return users from PostgreSQL followed by MongoDB |
GET |
/api/users/{id} |
200 |
Return one PostgreSQL user by ID |
GET |
/api/users?username={value} |
200 |
Case-insensitive partial username search |
GET |
/api/users?name={value} |
200 |
Case-insensitive partial name or surname search |
POST |
/api/users |
201 |
Validate and create a PostgreSQL user |
GET |
/api/v2/users?page={page}&size={size} |
200 |
Return a stable, provenance-aware page |
GET |
/api/v2/users/{source}/{sourceId} |
200 |
Return one user by source-local identity |
POST |
/api/v2/users |
201 |
Create a PostgreSQL user and return its source identity |
When both filters are supplied, username takes precedence. Repeated reads are cached for five minutes; a successful POST invalidates all search variants. The compatibility API returns at most 100 records per source. New clients should use v2, where page is 0..100, size is 1..100, and records are ordered by username, source, then source-local ID.
V2 keeps records from different stores distinct even when usernames match:
{
"items": [
{
"source": "postgresql",
"sourceId": "123e4567-e89b-12d3-a456-426614174000",
"username": "johndoe",
"firstName": "John",
"lastName": "Doe"
}
],
"page": 0,
"size": 20,
"hasNext": false
}curl "http://127.0.0.1:8080/api/users?username=user"
curl -i -X POST "http://127.0.0.1:8080/api/users" \
-H "Content-Type: application/json" \
-d '{"username":"johndoe","name":"John","surname":"Doe"}'The service does not silently return partial data. If either source fails or exceeds the configured timeout, the request returns 503 with a correlation ID. Logs identify the source and exception type without copying exception messages that may contain credentials.
HTTP adapters: /api/users and /api/v2/users
|
request/response DTO mapping
|
UserAggregationService + cache
|
UserReadSource / UserWriter ports
| |
PostgreSQL adapter MongoDB adapter
| |
JPA entity + Flyway Mongo document
The core dependency direction is enforced by ArchUnit. Application/domain code cannot depend on controllers, HTTP DTOs, or persistence adapters. CI additionally starts disposable PostgreSQL and MongoDB containers and verifies the real Flyway, JPA, MongoDB, and aggregation contract together.
| Concern | Entry point |
|---|---|
| Liveness/readiness | /actuator/health/liveness, /actuator/health/readiness |
| Metrics | /actuator/metrics |
| API contract | /v3/api-docs |
| Local image health | Docker health check against /actuator/health |
Health details are not exposed to unauthenticated callers. Logs contain source failures and server-side correlation IDs, but API errors do not expose exception messages.
After a source query, /actuator/metrics/user.aggregation.source.query exposes
duration and count with bounded source and outcome tags.
- The HTTP API has no authentication or authorization.
- MongoDB schema evolution is application-managed; Flyway covers only the relational schema.
- This repository does not define production deployment, backups, SLOs, alerting, or disaster recovery.
- Cached responses are local to one application instance.
See SECURITY.md for supported versions and private vulnerability reporting. Do not commit .env, credentials, database dumps, or generated target/ content.
See CONTRIBUTING.md. Pull requests must pass Java tests, the real PostgreSQL and MongoDB Testcontainers contract, architecture rules, the package build, and the Docker image build.
Licensed under the MIT License. You may use, modify, and distribute the software as long as the copyright notice and license text are retained.