Orange Promotional Gift Fulfilment
A production-style promotional fulfilment engine designed to grant a gift exactly once under concurrent requests.
- Role
- Backend architecture and implementation
- Status
- Internship deliverable — non-production data
- Stack
- Java 21, Spring Boot, PostgreSQL
The integration test issues five simultaneous fulfilment requests for one eligible user and asserts exactly one grant, with the rest returning a consistent replay.
Overview
A promotional campaign gives eligible subscribers a gift — mobile credit, in this case — subject to frequency caps. The engineering problem is not the giving. It is that mobile networks retry, users double-tap, and clients time out and resubmit, so the same request arrives several times at once, and the system has to grant exactly one gift anyway.
Built during an internship at Orange Egypt as a production-style design, running against synthetic campaign data.
Problem
Eligibility and fulfilment must agree under concurrency. It is easy to write a system where
GET /eligibility says yes, and then two simultaneous POST /fulfil calls both read "not
yet redeemed", both pass the cap check, and both grant. The gap between check and write is
where the money leaks.
My role
Backend architecture and implementation.
Data model
Campaigns, gifts by segment, segment assignments, cap rules, idempotency records, and an immutable redemption ledger. The ledger is append-only by design: a redemption is a fact that happened, not a row to be mutated. Uniqueness constraints at the schema level make the "exactly once" property a database invariant rather than an application convention.
API surface
Four endpoints: eligibility preview, transactional fulfilment, segment creation, and segment assignment.
Eligibility is a preview, and is named that way deliberately. It answers "would this succeed right now", which is a different and weaker statement than "this will succeed". Only fulfilment is authoritative.
Exactly-once design
Four mechanisms, in order:
- Request key. The caller supplies an idempotency key. A repeat of a completed request short-circuits to a fast replay path and returns the original outcome, without touching the ledger.
- Advisory transaction lock. Before doing anything that matters, the transaction takes a PostgreSQL advisory lock scoped to the user and campaign. Competing requests serialise here rather than racing.
- Re-check after acquiring the lock. This is the step people skip. Passing the idempotency check before waiting on a lock proves nothing, because the request that was ahead in the queue may have completed in the meantime. The check is repeated once the lock is held.
- Immutable ledger with schema-level uniqueness. If all three previous layers somehow failed, the database still refuses the duplicate.
Layers 1–3 are for correctness under normal contention. Layer 4 is for being wrong about layers 1–3.
Cap rules as data
Cap rules are rows, not code: X redemptions per Y days, attached to a gift. The demo campaign uses one-per-day for the first segment, and one-every-three-days and at most two-per-week for the second — both of which must pass.
Making this data-driven means a new segment or a changed frequency cap ships as a configuration change rather than a redeploy. On a promotional campaign, where rules change mid-flight in response to uptake, that is the difference between a marketing decision taking minutes and taking a release cycle.
Failure handling
- Repeated identical request: fast replay, original outcome, no new ledger entry
- Simultaneous competing requests: serialised on the advisory lock, one wins, the rest replay
- Cap boundary: denied with a reason code rather than a generic failure, so the caller can tell the user why
- Provider outcome: the fulfilment provider is behind an interface, so its side effects are separable from the ledger write
Test design
GiftFulfillmentIntegrationTest issues five simultaneous fulfilment calls for one eligible
user and asserts exactly one grant with consistent replay for the rest, and separately
exercises the cap boundary.
Concurrency bugs do not show up in single-threaded tests, so the test harness has to actually contend. Firing five real simultaneous requests at one user is the smallest setup that would catch a missing lock or a check-then-write gap.
Security and privacy
The seed data is synthetic: MSISDNs 201000000001 through 201000000003, a fictional
WORLDCUP_2026 campaign, and gifts of 20 and 50 EGP.
Scope
The advisory-lock approach coordinates within a single PostgreSQL instance, which is the right scope for one campaign engine and the reason the design stays this simple. The fulfilment provider sits behind an interface, so its side effects are cleanly separable from the ledger write.
Where I would take this next
- An outbox with provider-side idempotency, so the ledger and the external side effect commit as one story even when the provider call and the transaction disagree.
- A retained load-test artifact, turning the concurrency design into a measured throughput and latency profile.
- Metrics on lock wait time and replay rate — the two numbers that would tell an operator how much contention the campaign is actually producing.
- Flyway or Liquibase for versioned schema migrations.
Constraints
- A retry storm must not grant two gifts
- Eligibility and fulfilment must never disagree
- Campaign rules change often, so they cannot be compiled in
- The redemption record has to be auditable after the fact
Artifacts
- REPORTArchitecture and design
The data model, the exactly-once sequence and the test design, written up below.
Notes on evidence
- A production-style engineering demo built during an internship against synthetic data.