What was exercised
Integration, not mocks
Testcontainers starts Postgres 16, Flyway applies the real migrations, and the tests drive the running server over HTTP — including the partial unique index that makes idempotency safe.
One domain, two transports
REST and GraphQL share services and repositories. Errors do not: REST returns RFC 9457 problem details, GraphQL returns typed errors with a code.
Deterministic demo
Seed data uses fixed UUIDs and prices, so the recordings below are reproducible rather than incidental.
Load test
A short k6 run (30s ramp, 10 VUs peak) against the same running process. The point is that the endpoints answer correctly under concurrency, not that the numbers are a benchmark.
Loading k6 summary…
Error model
| Failure | REST | GraphQL |
|---|---|---|
| Unknown customer or product | 404, type …/not-found, resource and id | error with extensions.code = NOT_FOUND |
| Invalid body or field | 400, type …/validation-failed, field map in errors | extensions.code = INVALID_REQUEST |
| Illegal status transition | 409, type …/conflict | extensions.code = CONFLICT |
| Retried create with same key | 200 with the original order and Idempotent-Replay: true | replayed: true on the payload |
Design decisions
- JDBC over JPA — explicit SQL, no lazy-loading surprises, and it pairs with the migration lab.
- REST and GraphQL together — one service layer, two contracts, two error shapes.
- Idempotency keys — a partial unique index, not an application-level lock.
- Transactional outbox — the event commits with the order or not at all.