ClickNBack: Building a Cashback System That Earns Its Complexity

Posted on Mar 27, 2026

Last week I explained why I dropped a marketplace for a cashback system. If you read Monday’s post, you already know where to find the ADRs, the tests, and why the engineering discipline matters. What you don’t know yet is what problem this system is actually solving—and why that problem is worth the complexity.

So let’s talk about the domain itself. Cashback at first looks simple: user buys something, user gets money back. That surface simplicity is exactly the lie that makes the space interesting.

A transaction flows through multiple parallel systems: reconciliation, wallet balance, audit logging, and cashback calculation—each with its own complexity layer.

Why Simplicity Is Dangerous Here

The lie is that money is straightforward to track. In cashback platforms, it’s not. Every monetary value must be a Decimal—floating-point arithmetic has never been involved with a wallet balance, and it never will be. That’s constraint number one.

Constraint number two: purchases don’t get confirmed inline. A user makes a purchase at a partner merchant, and the transaction is ingested optimistically—held in a pending state while a background job simulates bank reconciliation. Only after confirmation publishes an event does cashback get credited. That pattern raises questions that don’t exist in simpler systems:

  • What’s the wallet state while confirmation is pending? (Answer: three separate buckets—pending, available, paid.)
  • What happens if a purchase is reversed after cashback is already credited? (Answer: an inverse transaction that mirrors the original state change.)
  • How do you prevent the same confirmation signal from crediting cashback twice? (Answer: idempotency enforced by UNIQUE at the database level, not just application logic.)
  • What if a user requests a withdrawal at the exact moment pending cashback confirms? (Answer: SELECT FOR UPDATE row-level locking.)

These aren’t edge cases. In financial systems, they’re the main event. Every one of them.Designing for them from the ground up isn’t optional—it’s the minimum bar for correctness. That’s why a “simple” cashback platform demands the architectural discipline that a typical CRUD application can ignore entirely.

What the System Actually Does

The system flow itself is straightforward in concept: ingest → confirm → calculate → credit → withdraw. In practice, each step echoes back to those constraints.

The happy path: A user makes a purchase; the platform creates a pending transaction. A background job simulates bank confirmation—retrying up to a configurable limit before giving up. On success, it publishes an event to an internal message broker. A separate subscriber picks that up, applies the offer rules, and credits cashback to the three-state wallet. The user can then request a withdrawal, and the balance moves from available to paid. Every critical operation appends a permanent audit row.

The hard parts: Offer rules themselves have guardrails—a percentage cap, a fixed amount cap, or a per-user monthly ceiling. Merchant definitions are strict: a purchase is tied to exactly one merchant, exactly one offer. Feature toggles let you flip capabilities without a deployment. And the background job isn’t naive—it uses a Fan-Out Dispatcher pattern where each pending purchase gets its own independent retry lifecycle. One confirmation taking 30 minutes doesn’t poison the batch queue.

The system sits behind that architecture. Which is why, if you read Monday’s post, you now know why the architecture is enforced and not suggested.

Go Kick the Tires

The API is live at clicknback.com/docs. Interactive Swagger UI, demo credentials, a fresh database every night. No setup needed—just hit the endpoints.

The point of that isn’t show. It’s honesty. A live, running system that ships on every passing commit is a clearer proof point than a repository with a perfect README. You can see what the system actually does, how it fails, and what patterns hold up under real load.

Of course, “exposed to the real internet” is a phrase I learned to respect quite quickly. That story—the one about discovery through chaos—is coming next week. For now, the system is waiting. Read the code if you want the architecture. Run a transaction if you want the domain.