ADRs document why we made a decision, not just what we did. We use the Michael Nygard format. Each ADR has a status: Proposed, Accepted, Superseded.

Convention: ADR ids are 4-digit numbers (ADR-0001). Once Accepted, do not edit — write a new ADR that supersedes it.


ADR-0001 — Use Spring Boot 3 + Java 17 for all services

Status: Accepted

Context. The original challenge is Spring Boot. The team has Java expertise. Java 17 has long-term support through 2029.

Decision. All JVM services in this codebase use Java 17 and Spring Boot 3.x. Cross-cutting libraries (Spring Cloud, Resilience4j, Micrometer) come from the Spring ecosystem.

Consequences. - Pros: shared mental model, large hire pool, mature libraries. - Cons: heavier images than Go/Rust; we mitigate with native image (GraalVM) for some services in a future ADR.


ADR-0002 — Multi-module Gradle, single repository

Status: Accepted

Context. The team is small. We want easy cross-service refactors. We do not want a separate repository per service yet.

Decision. A single Git repository (monorepo) with a multi-module Gradle build. Each services/<name>/ is an independently buildable module; CI builds only changed modules using path filters.

Consequences. - Pros: atomic cross-service refactors, one PR can update a contract and both consumers; shared tooling. - Cons: longer CI builds without path filtering; access control must be at directory level (CODEOWNERS). - We will revisit this if/when teams grow such that they need different release cadences they cannot coordinate.


ADR-0003 — One database per service

Status: Accepted

Context. The original monolith uses an in-memory store. Splitting into microservices, we must decide the data ownership model.

Decision. Each stateful service owns its own PostgreSQL schema. No service reads or writes another service's tables directly. payment-service writes the payments table; payment-query-service writes payments_view (rebuilt from events).

Consequences. - Pros: services can evolve independently; no data-layer coupling; clear ownership. - Cons: cross-service queries become API calls or event-driven projections; reporting needs an ETL or a dedicated read store.


ADR-0004 — Eventual consistency between write- and read-side

Status: Accepted

Context. With CQRS, the read model lags behind the write model.

Decision. Accept eventual consistency on the read side. Document the SLA: "p99 propagation lag of new payments ≤ 1 s". Provide a Location header on POST /api/payments so clients can implement read-after-write retries.

Consequences. - Pros: high read throughput, denormalised view, replayable. - Cons: clients must handle "not yet visible" reads; explicit testing for the lag boundary.


ADR-0005 — Transactional outbox for Kafka publishes

Status: Accepted

Context. payment-service must persist to Postgres and publish a PaymentAuthorised event to Kafka atomically. JTA across Postgres and Kafka is not a real option.

Decision. Use the transactional outbox pattern. Inside one DB transaction, write the payment row and an outbox row. A scheduled poller drains outbox rows to Kafka and marks them published.

Consequences. - Pros: at-least-once delivery without two-phase commit; survives crashes between commit and publish. - Cons: consumers must be idempotent; small additional latency until the poller picks up. - Alternatives considered: Debezium CDC on the payments table — heavier infra, deferred until justified by scale.


ADR-0006 — Synchronous REST for fraud-check and bank-adapter calls

Status: Accepted

Context. payment-service needs the fraud decision and the bank authorisation result before responding to the merchant.

Decision. These two calls are synchronous REST (RestClient). They are wrapped with Resilience4j TimeLimiter, Retry, CircuitBreaker, Bulkhead.

Consequences. - Pros: simple semantics; matches the request/response nature of the operation. - Cons: failure of either dependency translates to a failure of the payment unless we degrade gracefully. We do degrade fraud-check to decision=REVIEW on failure (configurable).


ADR-0007 — Spring Cloud Gateway as the API edge

Status: Accepted

Context. We need one entry point for routing, JWT validation, rate limiting, CORS, request logging.

Decision. Use Spring Cloud Gateway (reactive). Routes are defined declaratively in YAML.

Consequences. - Pros: in-ecosystem, easy custom filters in Java; fewer moving parts than a separate Envoy. - Cons: Reactive code is unfamiliar to MVC developers; we keep gateway logic minimal. - Alternatives considered: AWS API Gateway, Kong, Envoy + Istio. Rejected for the demo because they couple too tightly to a specific environment.


ADR-0008 — Eureka for local discovery, Cloud Map in AWS

Status: Accepted

Context. Services must find each other.

Decision. Use Spring Cloud Eureka locally (familiar, easy). In AWS, switch to AWS Cloud Map with ECS Service Connect — same DiscoveryClient interface, no code changes.

Consequences. - Pros: works locally without AWS; production uses managed AWS service. - Cons: two registries to understand; mitigated by abstracting through Spring's DiscoveryClient.


ADR-0009 — JWT validation at the gateway, claim propagation downstream

Status: Accepted

Context. Every request must be authenticated. Downstream services must know the merchant.

Decision. External JWTs (issued by IdP) are validated at the api-gateway only. The gateway extracts merchantId and subject, drops the bearer token, and forwards a stripped-down X-Merchant-Id and X-User-Id header. Service-to-service calls use mTLS (in AWS) or short-lived OAuth2 client-credentials tokens (alternative).

Consequences. - Pros: downstream services don't reimplement JWT logic; bearer token never leaks past the edge. - Cons: any service exposed without going through the gateway must independently authenticate. We enforce this with security groups + private subnets.


ADR-0010 — Resilience4j over Hystrix

Status: Accepted

Context. Hystrix is in maintenance mode.

Decision. Use Resilience4j for circuit breakers, retries, time limiters, bulkheads, and rate limiters. Configure per-instance via Spring Boot YAML.

Consequences. - Pros: actively maintained; lightweight; works with Micrometer; supports both annotations and functional API. - Cons: configuration is verbose; we standardise base settings as YAML profiles.


ADR-0011 — Flyway for database migrations

Status: Accepted

Context. Each service owns its schema and must evolve it safely.

Decision. Use Flyway to manage migrations. Each service has its own db/migration/ folder. Migrations run on application startup.

Consequences. - Pros: forward-only migrations enforced; per-service schema lifecycle. - Cons: long migrations on big tables can slow boot; we offload heavy migrations to one-off jobs.


ADR-0012 — Logback structured JSON logs + W3C Trace Context

Status: Accepted

Context. Distributed debugging needs correlation across services.

Decision. All services log JSON via logstash-logback-encoder. Trace context is propagated using W3C traceparent headers on HTTP and Kafka headers traceparent / tracestate. We use Micrometer Tracing → OpenTelemetry → OTLP exporter.

Consequences. - Pros: vendor-neutral; one trace across REST and Kafka; easy ingestion to ELK, Loki, CloudWatch. - Cons: trace context lost across some legacy clients; we add a fallback X-Correlation-Id.


ADR-0013 — Deploy on AWS ECS Fargate (not EKS) for v1

Status: Accepted

Context. Eight services, small ops team, no existing K8s expertise.

Decision. Deploy on ECS with Fargate in v1. Reassess if we hit Fargate limits, need advanced scheduling, or the team grows K8s skills.

Consequences. - Pros: lowest operational tax; native AWS integrations (IAM roles per task, Secrets Manager, ALB target groups). - Cons: ECS is AWS-only; we accept the lock-in for now and keep the application code itself cloud-agnostic.


ADR-0014 — MSK for Kafka in production, Redpanda or Bitnami Kafka locally

Status: Accepted

Context. We need a durable event bus.

Decision. Use Amazon MSK (managed Kafka) in AWS environments. Locally, run Redpanda (Kafka-compatible, lighter) or bitnami/kafka via Docker Compose.

Consequences. - Pros: managed brokers in prod; identical client code locally and in AWS. - Cons: MSK is the most expensive piece of the stack — for very small dev environments we use MSK Serverless or Amazon MQ for ActiveMQ as a cheaper alternative.


ADR-0015 — payment-events shared library, semver-strict

Status: Accepted

Context. Producers and consumers must agree on event shapes.

Decision. Publish a payment-events library to our internal Maven repo (or git as artifact). Versioned with semver. Backward-incompatible changes require a major bump and a coexistence period.

Consequences. - Pros: compile-time guarantee on event contracts. - Cons: all consumers must upgrade for breaking changes — we offset this with forward-compatibility rules (Jackson @JsonIgnoreProperties(ignoreUnknown = true)) and never-rename-or-remove discipline. - Alternative considered: a Schema Registry (Confluent / AWS Glue) with Avro/Protobuf. Better for many languages; deferred until needed.