Production-grade Spring Boot + Spring Cloud Kubernetes microservices — not a tutorial, a working system.
Four services (gateway, organization, department, employee) deploy to isolated namespaces on a local Kind cluster with one command. The stack is fully wired: cross-namespace service discovery, inter-service REST calls via @HttpExchange, MongoDB persistence, distributed tracing, and an API gateway with Swagger UI. CI runs 7 pipeline stages including Trivy CVE scans, Testcontainers integration tests, Kind-based e2e tests, and multi-arch Docker builds with SLSA provenance + cosign signing. Everything is make-driven — make kind-up to run it, make ci to validate it, make kind-down to tear it down.
Source: docs/diagrams/c4-container.puml — PlantUML + C4-PlantUML with a modern flat skinparam block (no shadows, sharp corners, Inter font, teal/indigo/violet palette). Regenerate with make diagrams.
| Component | Technology | Rationale |
|---|---|---|
| Language | Java 25 | Current LTS-grade release; virtual threads + pattern matching + records make Spring Boot 4 code terser and more concurrent |
| Framework | Spring Boot 4.0, Spring Cloud 2025.1 | Mainstream Java microservices stack; Spring Boot 4 drops Spring Boot 2.x compat, adopts Jakarta EE 10, cleaner auto-configuration |
| API Gateway | Spring Cloud Gateway Server WebMVC | Servlet-stack gateway (not reactive WebFlux) — simpler mental model, easier to instrument, matches the blocking RestClient used elsewhere |
| Inter-service | RestClient with @HttpExchange |
Native Spring declarative HTTP client; replaces Feign without pulling Netflix OSS; works with Spring Cloud LoadBalancer for service-discovery-aware calls |
| Service Discovery | Spring Cloud Kubernetes | Uses the Kubernetes API as the registry — no Eureka/Consul to operate; all-namespaces: true enables cross-namespace discovery |
| Database | MongoDB 8.0 (official mongo image, non-root UID 999, version-pinned) |
Document model fits the Organization → Department → Employee aggregates without a migration toolchain; official image pinned for Renovate tracking |
| API Docs | SpringDoc OpenAPI 3.0 / Swagger UI | Auto-generates OpenAPI 3 from @RestController annotations; gateway surfaces a unified Swagger UI across all services |
| Tracing | Micrometer Tracing → OpenTelemetry OTLP → Jaeger | Spans propagate across all four services via W3C traceparent, export over OTLP/HTTP to in-cluster Jaeger (observability namespace); UI via make jaeger-open |
| Testing | Testcontainers (integration), Kind e2e | Real MongoDB per test class (no mocking) + real cluster for e2e — catches schema drift and manifest bugs that in-process tests miss |
| Containers | Eclipse Temurin 25, multi-arch (amd64+arm64) | Temurin is the reference OpenJDK build; multi-arch covers Apple Silicon dev + x86 servers from a single manifest |
| Local K8s | Kind + MetalLB | Kind runs a real Kubernetes API in Docker — higher fidelity than Minikube; MetalLB gives LoadBalancer Services a reachable IP on localhost |
| CI/CD | GitHub Actions, Renovate, GHCR | GitHub-native, zero extra infrastructure; Renovate auto-merges minor/patch dependency updates; GHCR avoids Docker Hub pull-rate limits |
| Code Quality | google-java-format, Checkstyle, hadolint, gitleaks, actionlint, Trivy, PlantUML | Composite make static-check gate — format + lint + Dockerfile lint + secret scan + workflow lint + filesystem/K8s config CVE scan + diagram drift — fails the build on any single violation |
make deps # check required tools
make kind-up # full cluster lifecycle: Kind + MetalLB + MongoDB + Jaeger + 4 services
make e2e-test # run end-to-end API tests
make gateway-open # open Swagger UI in browser
make jaeger-open # open Jaeger tracing UI in browser
make kind-down # tear everything down when finishedmake kind-up is an alias for make kind-deploy that chains
kind-create → kind-setup → image-build → image-load → service
deployment. See the granular targets below if you need to
run individual steps (e.g., to skip the image rebuild during iterative
development).
| Tool | Version | Purpose |
|---|---|---|
| GNU Make | 3.81+ | Build orchestration |
| Git | 2.0+ | Version control |
| JDK | 25 | Java runtime and compiler (source of truth: .java-version) |
| Maven | 3.9+ | Build and dependency management (pinned: MAVEN_VER in Makefile) |
| Docker | 20.10+ | Container runtime |
| kubectl | 1.24+ | Kubernetes CLI |
| Kind | 0.31+ | Local Kubernetes clusters (auto-installed by make deps-kind) |
| mise | latest | Polyglot version manager — installs Java, Maven, and Node per .mise.toml (optional, used by make deps-install) |
Verify required tools are installed:
make depsThis architecture follows Cloud Native best practices and The 12 Factor App methodology. Key concerns addressed:
- Externalized configuration using ConfigMaps, Secrets, and PropertySource
- Kubernetes API access using ServiceAccounts, Roles, and RoleBindings
- Health checks using readiness, liveness, and startup probes
- Application state reported via Spring Boot Actuators
- Service discovery across namespaces using Spring Cloud Kubernetes DiscoveryClient
- Inter-service communication via RestClient (
@HttpExchange) - API documentation exposed via Swagger UI
- Docker images built with layered JARs using the Spring Boot plugin
- Observability via Prometheus exporters + distributed tracing (Micrometer → OTLP → Jaeger)
- Static analysis via google-java-format, Checkstyle, hadolint, gitleaks, actionlint, Trivy (filesystem + K8s config), and PlantUML diagram drift check — all wired into the
make static-checkcomposite gate
Client -> Gateway (Spring Cloud Gateway Server WebMVC, LoadBalancer via MetalLB)
|-- /employee/** -> Employee Service (MongoDB)
|-- /department/** -> Department Service (MongoDB, calls Employee via RestClient)
+-- /organization/** -> Organization Service (MongoDB, calls Department + Employee via RestClient)
Each service runs in its own Kubernetes namespace with dedicated service accounts and RBAC role bindings for cross-namespace discovery.
GET /organization/{id}/with-departments-and-employees hits three services and the datastore in a single request. The sequence:
sequenceDiagram
autonumber
participant C as Client
participant G as Gateway
participant O as Organization
participant D as Department
participant E as Employee
participant M as MongoDB
C->>G: GET /organization/1/with-departments-and-employees
G->>O: forward (Spring Cloud Gateway route)
O->>M: find organization by id
M-->>O: Organization
O->>D: GET /department/organization/1/with-employees (@HttpExchange)
D->>M: find departments by organizationId
M-->>D: Department[]
D->>E: GET /employee/department/{id} (@HttpExchange, once per department)
E->>M: find employees by departmentId
M-->>E: Employee[]
E-->>D: Employee[]
D-->>O: Department[] with embedded employees
O-->>G: Organization with nested departments + employees
G-->>C: 200 application/json
Steps 3–10 all happen inside the cluster via ClusterIP Services resolved through Spring Cloud Kubernetes DiscoveryClient. The gateway only sees the outer request (1) and final response (13). Trace headers (traceparent) propagate through every hop via Micrometer Tracing.
See the full Reference Architecture for the Deployment diagram, Kubernetes DNS table, and per-manifest configuration details, and Architecture Decision Records for the rationale behind key choices.
Local Kubernetes deployment is driven by the Makefile — make kind-up spins up the full stack in one command:
make kind-up # Kind cluster + MetalLB + MongoDB + Jaeger + 4 services (~2–3 min)
make gateway-url # print LoadBalancer IP assigned by MetalLB
make gateway-open # open Swagger UI in a browser
make jaeger-open # open Jaeger tracing UI in a browser
make kind-down # tear everything downUnder the hood kind-up chains kind-create (cluster + MetalLB) → kind-setup (namespaces, RBAC, MongoDB, Jaeger) → image-build → image-load → kind-deploy (rollout all 4 services). See Kind Cluster targets for running each step in isolation during iterative development.
Production deployment is out of scope for this reference — the manifests under k8s/ are tuned for a single-node local Kind cluster. See docs/reference-architecture.md for the annotated manifests and the rationale behind each ConfigMap / Secret / RBAC binding.
The gateway exposes a unified surface on http://<GATEWAY_IP>:8080 (MetalLB-assigned). Fetch the IP with make gateway-url, then:
GATEWAY=$(make --silent gateway-url)
# Seed some data
make populate
# Employees CRUD
curl -s "http://$GATEWAY:8080/employee/"
curl -s "http://$GATEWAY:8080/employee/department/1"
# Cross-service fan-out — organization 1, fully expanded with departments + employees
curl -s "http://$GATEWAY:8080/organization/1/with-departments-and-employees" | jqA complete OpenAPI 3 spec plus Swagger UI is served through the gateway — run make gateway-open to launch it, or point a browser at http://<GATEWAY_IP>:8080/swagger-ui/index.html. See e2e/e2e-test.sh for exhaustive end-to-end assertions across every route.
Run make help to see all available targets.
| Target | Description |
|---|---|
make build |
Build all modules with Maven (skip tests) |
make clean |
Clean all build artifacts |
make test |
Run tests |
make format |
Auto-format Java source code (Google style) |
make format-check |
Verify code formatting (CI gate) |
| Target | Description |
|---|---|
make static-check |
Run all quality and security checks (format-check, diagrams-check, mermaid-lint, lint-ci, lint, lint-docker, secrets, trivy-fs, trivy-config) |
make lint |
Run Maven validate, compiler warnings-as-errors, and Checkstyle (google_checks.xml) |
make lint-ci |
Lint GitHub Actions workflows with actionlint (uses shellcheck) |
make lint-docker |
Lint all Dockerfiles with hadolint |
make secrets |
Scan for hardcoded secrets |
make trivy-fs |
Scan filesystem for vulnerabilities, secrets, and misconfigurations |
make trivy-config |
Scan Kubernetes manifests for security misconfigurations (KSV-*) |
make diagrams |
Render PlantUML architecture diagrams under docs/diagrams/ to PNG |
make diagrams-check |
Verify committed PNGs match current .puml source (drift check for CI) |
make cve-check |
Run OWASP dependency vulnerability scan |
make coverage-generate |
Generate code coverage report |
make coverage-check |
Verify code coverage meets minimum threshold |
make coverage-open |
Open code coverage report in browser |
| Target | Description |
|---|---|
make image-build |
Build Docker images for all services |
make image-load |
Load Docker images into KinD cluster |
| Target | Description |
|---|---|
make kind-up |
Full cluster lifecycle (alias for kind-deploy): create + MetalLB + setup + image build + deploy |
make kind-down |
Tear down the Kind cluster (alias for kind-destroy) |
make kind-create |
Create local KinD cluster with MetalLB (granular) |
make kind-setup |
Create namespaces, RBAC, service accounts, and deploy MongoDB (granular) |
make kind-deploy |
Build, load images, deploy all services, and wait for rollout (granular) |
make kind-undeploy |
Remove all services from KinD cluster (keeps cluster running) |
make kind-redeploy |
Undeploy then deploy all services |
make kind-destroy |
Delete KinD cluster (granular) |
| Target | Description |
|---|---|
make e2e |
Run full end-to-end test cycle (create, setup, deploy, test, destroy) |
make e2e-test |
Run end-to-end test script |
make populate |
Populate test data via gateway |
| Target | Description |
|---|---|
make help |
List all available targets |
make gateway-url |
Print gateway LoadBalancer URL |
make gateway-open |
Open Swagger UI in browser |
make jaeger-open |
Open Jaeger tracing UI in browser |
make logs-employee |
Tail employee service logs |
make logs-department |
Tail department service logs |
make logs-organization |
Tail organization service logs |
make logs-gateway |
Tail gateway service logs |
| Target | Description |
|---|---|
make ci |
Run full local CI pipeline (deps, static-check, coverage, build, deps-prune-check) |
make ci-run |
Run GitHub Actions workflow locally via act |
make release VERSION=x.y.z |
Create a release (usage: make release VERSION=x.y.z) |
make maven-settings-ossindex |
Create Maven settings for OSS Index credentials |
| Target | Description |
|---|---|
make deps |
Check required tools (java 25, mvn) |
make deps-install |
Install mise and the toolchain pinned in .mise.toml (Java, Maven, Node) |
make deps-maven |
Install Maven if not present (for CI containers) |
make deps-check |
Show required tools and installation status |
make deps-docker |
Check Docker (used by diagrams, mermaid-lint, image-build, Testcontainers) |
make deps-kubectl |
Check kubectl (required for Kind cluster targets) |
make deps-kind |
Install KinD for local Kubernetes testing |
make deps-act |
Install act for local CI runs |
make deps-hadolint |
Install hadolint for Dockerfile linting |
make deps-gitleaks |
Install gitleaks for secret scanning |
make deps-trivy |
Install Trivy for vulnerability and misconfig scanning |
make deps-actionlint |
Install actionlint for GitHub Actions linting |
make deps-shellcheck |
Install shellcheck (used by actionlint) |
make deps-updates |
Print project dependencies updates |
make deps-update |
Update project dependencies to latest releases |
make deps-prune |
Check for unused Maven dependencies |
make deps-prune-check |
Fail if unused/undeclared Maven dependencies are present (CI gate) |
| Target | Description |
|---|---|
make renovate-bootstrap |
Install mise + Node (per .nvmrc) for renovate-validate |
make renovate-validate |
Validate Renovate configuration |
GitHub Actions runs on every push to master, tags v*, and pull requests.
| Job | Triggers | Steps |
|---|---|---|
| static-check | push, PR | make static-check composite gate: format-check, diagrams-check, mermaid-lint, lint-ci (actionlint), lint (Checkstyle + compiler warnings-as-errors), lint-docker (hadolint), secrets (gitleaks), trivy-fs, trivy-config |
| build | after static-check | Build all modules with Maven, upload JARs as service-jars artifact |
| test | after static-check | Run Testcontainers integration tests + coverage (non-blocking) |
| cve-check | push to master AND tag pushes (skipped under act) |
OWASP dependency vulnerability scan — gates the docker job on tag pushes |
| image-scan | every push (matrix: 4 services) | Per-service Dockerfile validation gates 1–3: build single-arch image → Trivy image scan (CRITICAL/HIGH blocking) → Spring Boot boot-marker smoke test. Catches base-image CVE regressions and Dockerfile breakages on the commit that introduced them, not on release day. |
| e2e | every push (skipped under act) |
End-to-end test against a full Kind + MetalLB stack: make e2e cycles create → setup (MongoDB) → deploy (4 services + gateway LB) → ./e2e/e2e-test.sh → destroy. |
| docker | tag push only (matrix: 4 services) | Full pre-push hardening: build local image → Trivy image scan → Spring Boot smoke test → multi-arch (amd64+arm64) build with SLSA provenance + SBOM attestation → push to GHCR → cosign keyless OIDC signing. Depends on build, test, cve-check. |
| ci-pass | always | Branch-protection aggregator: single required status check that verifies no upstream job failed. Skipped jobs do not trip the gate. |
The docker job runs the following gates before any image is pushed to GHCR. Any failure blocks the release.
| # | Gate | Catches | Tool |
|---|---|---|---|
| 1 | Build local single-arch image | Build regressions on the runner architecture | docker/build-push-action with load: true |
| 2 | Trivy image scan (CRITICAL/HIGH blocking) | CVEs in the base image (eclipse-temurin:25-jre-noble), OS packages, and any layers added during the build that the filesystem scan can't see |
aquasecurity/trivy-action with image-ref: |
| 3 | Spring Boot boot-marker smoke test | Image is well-formed: JVM starts, Spring context boots, embedded Tomcat begins listening (greps the container logs for Started <Service>Application in N.NN seconds within 90s — no MongoDB needed since we don't gate on /actuator/health) |
docker run + docker logs + grep |
| 4 | Multi-arch build + push | Publishes for both linux/amd64 and linux/arm64. Mostly cache-hit from gate 1. |
docker/build-push-action |
| 5 | SLSA L2 build provenance (provenance: mode=max) |
Cryptographic record of how the image was built (commit, builder, build args). Verifiable via the OCI manifest. | docker/build-push-action native attestation |
| 6 | SBOM attestation (sbom: true) |
Software Bill of Materials embedded in the image manifest as an attestation, auditable by Trivy/Grype/Syft consumers | docker/build-push-action native attestation |
| 7 | Cosign keyless OIDC signing | Sigstore signature on the manifest digest with no long-lived private keys (uses GitHub OIDC → Fulcio → Rekor) | sigstore/cosign-installer + cosign sign --yes |
Verify a published image's signature with:
# Replace <tag> with a published tag (e.g. 2.2.0, 2.2, 2, latest)
cosign verify ghcr.io/AndriyKalashnykov/spring-microservices-k8s/employee:<tag> \
--certificate-identity-regexp 'https://github\.com/AndriyKalashnykov/spring-microservices-k8s/.+' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comNote: GHCR's Packages UI shows extra unknown/unknown rows alongside the platform manifests — these are the attestation manifests (SLSA provenance + SBOM). They're cosmetic; docker pull works identically.
Integration tests use Testcontainers with MongoDB for fast local testing via make test.
End-to-end tests validate the full stack on Kind via make e2e.
| Name | Type | Used by | How to obtain |
|---|---|---|---|
NVD_API_KEY |
Secret | cve-check job |
Free API key from NIST NVD. Without it, OWASP dependency-check is heavily rate-limited. |
OSS_INDEX_USER |
Secret | cve-check job |
Free account at Sonatype OSS Index. Your email address. Optional — improves vulnerability data quality. |
OSS_INDEX_TOKEN |
Secret | cve-check job |
API token from OSS Index settings. Optional — paired with OSS_INDEX_USER. |
ACT |
Variable | cve-check job |
Set to true to skip the cve-check job during local act runs (set automatically by make ci-run). |
Set secrets via Settings > Secrets and variables > Actions > New repository secret. Set variables via Settings > Secrets and variables > Actions > Variables tab > New repository variable.
A weekly cleanup workflow prunes old workflow runs and stale caches.
Renovate keeps dependencies up to date with platform automerge enabled.
Contributions welcome — open a PR. Run make static-check locally before pushing; the full pipeline is reproducible via make ci (and make ci-run to exercise the GitHub Actions workflow through act).
MIT.
