exit lab
Backend Engineering
Level 12·capstone·case-study·advanced

Production Case Study: B2B Voucher Redemption

Design a fictional modular-monolith voucher platform with atomic redemption, idempotency, audit history, settlement outbox, security, testing, delivery, and operations.

90 minutes Fictional reference architecture using Java 21 and Spring Boot 3.5; no employer-confidential system details Updated 2026-07-16
Next.jsJava 21Spring BootMyBatisPostgreSQLRedisDocker

Learning objectives

  • Design an atomic and idempotent voucher redemption boundary
  • Connect architecture decisions to tests, deployment, monitoring, incidents, and rollback

What it is

This fictional B2B Voucher and Settlement Platform lets authorized issuers create voucher batches, merchants validate and redeem vouchers, operations users review exceptions, and finance users export settlement reports. It is a learning architecture, not a copy of any employer system.

Why it matters

Voucher redemption concentrates backend concerns: a code is sensitive, concurrent requests must not redeem twice, clients retry after timeouts, authorization spans tenant and merchant ownership, settlement is asynchronous, audit history matters, and operators need safe recovery.

How it works

Default modular-monolith architecture

Next.js client
Nginx / platform edge
Spring Boot API
Identity + Voucher + Redemption + Settlement modules
MyBatis
PostgreSQL
Outbox worker
Settlement provider
Text alternative: A Next.js client reaches the Spring Boot API through an edge proxy. Inside one deployable, identity, voucher, redemption, and settlement modules have explicit boundaries. MyBatis persists PostgreSQL state. An outbox worker later calls the fictional settlement provider.

Simple example

ActorAllowed capability
Issuer operatorCreate and activate batches for own tenant
Merchant cashierValidate and redeem for assigned merchant
Operations reviewerInvestigate exceptions with reason and audit trail
Finance analystRead settlement reports; cannot redeem
Platform administratorTenant administration with separately audited privilege

The API never accepts tenant or merchant authority merely because the client sends an ID. It derives actor scope from authenticated server-side identity and enforces ownership in service policy and SQL predicates.

Backend example

redemption-core.sql
CREATE TABLE voucher (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    tenant_id bigint NOT NULL,
    code_hash bytea NOT NULL,
    status varchar(20) NOT NULL CHECK (status IN ('ISSUED','REDEEMED','EXPIRED','CANCELLED')),
    expires_at timestamptz NOT NULL,
    redeemed_at timestamptz,
    version integer NOT NULL DEFAULT 0,
    CONSTRAINT voucher_tenant_code_uk UNIQUE (tenant_id, code_hash)
);

CREATE TABLE redemption (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    tenant_id bigint NOT NULL,
    merchant_id bigint NOT NULL,
    voucher_id bigint NOT NULL REFERENCES voucher(id),
    idempotency_key varchar(128) NOT NULL,
    request_hash bytea NOT NULL,
    result_code varchar(40) NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT redemption_idempotency_uk
        UNIQUE (tenant_id, merchant_id, idempotency_key)
);
redeem-voucher.sql
UPDATE voucher
SET status = 'REDEEMED',
    redeemed_at = :now,
    version = version + 1
WHERE id = :voucherId
  AND tenant_id = :tenantId
  AND status = 'ISSUED'
  AND expires_at > :now;

One affected row means the state transition won. Zero means the voucher is absent, out of scope, expired, or already terminal; the disclosure policy determines the external response. The idempotency unique key resolves concurrent replay races.

Production example

  1. 1

    Request

    Authenticate cashier, derive tenant/merchant scope, normalize and hash the voucher code, validate request, and compute a request fingerprint.

  2. 2

    Transaction

    Read any existing idempotency result, conditionally update the voucher, insert redemption and audit rows, and insert a settlement outbox event in one short transaction.

  3. 3

    Response

    Return the stored result for an exact replay. Reject reuse of the same key with a different request fingerprint.

  4. 4

    Async settlement

    A worker claims outbox records, calls a fictional provider with its own idempotency key and deadline, records attempt outcome, and retries bounded transient failures.

  5. 5

    Observe

    Measure redemption success/conflict/error rates, latency, database pool/lock state, outbox age, settlement failures, and security-denial patterns without logging raw voucher codes.

FailureExpected behavior
Client timeout after commitReplay returns stored redemption result
Two cashiers raceConditional update/unique constraint permits one business redemption
Settlement provider downRedemption stays committed; outbox retry remains visible
Worker crashes after provider successProvider idempotency and local attempt reconciliation prevent duplicate financial effect
Database pool exhaustedReadiness/load shedding and alerts protect recovery; no blind retry storm

Common mistakes

  • Storing or logging raw voucher codes without classification
  • Checking status and then updating in separate unprotected steps
  • Using Redis as the correctness boundary for redemption
  • Calling settlement inside the redemption transaction
  • Equating one message delivery with one business effect
  • Starting with microservices despite one team and one atomic core
  • Omitting restore and reconciliation procedures

Best practices

  • Hash sensitive lookup codes with an appropriate keyed design and document threat assumptions
  • Use constraints and conditional SQL as concurrency arbiters
  • Scope idempotency to actor and operation and bind it to the request
  • Keep immutable audit events separate from mutable operational status
  • Use outbox plus idempotent downstream behavior
  • Adopt expand/contract migrations
  • Run reconciliation and incident drills

Trade-offs

DecisionWhy chosenRevisit when
Modular monolithOne team, shared transaction, lower operational costModules need independent ownership, scale, or isolation
PostgreSQL is source of truthConstraints and transaction semanticsNever replace correctness with cache
Redis optionalRate limits or short-lived non-authoritative cacheOnly with measured need and fallback
OutboxAtomic business state and publish intentOperate relay, duplicates, and retention
Next.js clientTyped user/admin interfaceBackend contract remains client-independent

Interview questions

  • Prove that two concurrent redemptions cannot both succeed
  • What does the idempotency request hash protect?
  • Why is settlement outside the transaction?
  • How do you handle a worker crash after provider success?
  • Which data is safe to log?
  • When would you split the settlement module?
  • How do you reconcile provider and local state?

Hands-on task

  1. 1

    Milestone 1 — requirements and threat model

    Goal: define actors, flows, abuse, privacy, and non-goals. Acceptance: reviewed API/state diagrams and threat controls. Tests: misuse cases. Risks: copying confidential behavior. Production: name owners and audit scope.

  2. 2

    Milestone 2 — schema and migrations

    Goal: encode lifecycle, tenant scope, idempotency, audit, and outbox constraints. Acceptance: upgrade from empty and previous schema. Tests: constraint, plan, migration, and restore. Risks: incompatible enum/index change. Production: expand/contract and backup evidence.

  3. 3

    Milestone 3 — identity and authorization

    Goal: authenticate users and enforce role, tenant, merchant, and ownership policy. Acceptance: no client-controlled authority. Tests: 401, 403, horizontal access, CSRF/token cases. Risks: privilege leakage. Production: audit and revocation.

  4. 4

    Milestone 4 — atomic redemption

    Goal: one durable outcome per voucher and idempotent request. Acceptance: replay is stable and concurrent races create one effect. Tests: rollback, changed-payload conflict, and bounded concurrency. Risks: read/write race. Production: short transaction and explicit errors.

  5. 5

    Milestone 5 — settlement outbox

    Goal: reliable asynchronous delivery without holding database locks. Acceptance: visible retry and reconciliation state. Tests: duplicate delivery, provider timeout, crash-after-success. Risks: unbounded retry. Production: dead-letter/runbook and outbox-age alert.

  6. 6

    Milestone 6 — administration and reporting

    Goal: safe search, pagination, status operations, and export. Acceptance: scoped query plans and immutable audit access. Tests: authorization, keyset order, large export. Risks: data leakage. Production: retention and asynchronous export limits.

  7. 7

    Milestone 7 — quality and delivery

    Goal: unit/integration/security tests and immutable non-root delivery. Acceptance: CI gates and smoke-tested digest. Tests: image, workflow, deployment, graceful shutdown. Risks: migration/app mismatch. Production: approval and rollback rehearsal.

  8. 8

    Milestone 8 — operations

    Goal: logs, metrics, traces, dashboards, alerts, backup/restore, and incidents. Acceptance: pool-exhaustion and provider-outage drills. Tests: probes, alerts, restore, reconciliation, rollback. Risks: sensitive telemetry and alert fatigue. Production: runbook owners and postmortem actions.

References

  • Spring: Spring Boot 3.5.16 System Requirements — https://docs.spring.io/spring-boot/3.5/system-requirements.html
  • PostgreSQL Global Development Group: Transaction Isolation — https://www.postgresql.org/docs/18/transaction-iso.html
  • MyBatis: MyBatis Spring Boot Starter Reference — https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
  • Spring: Servlet Authentication Architecture — https://docs.spring.io/spring-security/reference/6.5/servlet/authentication/architecture.html
  • OWASP: Application Security Verification Standard 5.0 — https://owasp.org/www-project-application-security-verification-standard/
  • Microservices.io: Pattern: Transactional Outbox — https://microservices.io/patterns/data/transactional-outbox.html
  • OpenTelemetry: OpenTelemetry Concepts — https://opentelemetry.io/docs/concepts/

Retrieval check

Before you continue

  • Explain the main idea in your own words without rereading the page.
  • Name one production problem this knowledge helps prevent.
  • Describe where you would apply it in a Spring Boot project.

Source metadata

Primary references checked on 2026-07-16. Version scope is stated at the top of this page.