Case study, digital bank
Allocating 11,000 customer IDs a day with no duplicates
How I replaced sequence-based issuance of customer IDs in a digital bank's onboarding flow with pre-allocated pools.
The job
Every customer who opens an account at the bank gets a CIF number, the ID that follows them through every product. The bank has about 3 million customers, and the onboarding flow issues around 11,000 of these numbers a day. The previous design took them from a sequence, which kept them unique but made each one easy to guess from the last.
What the IDs had to do
- Stay unique. A duplicate CIF would mean two people sharing one banking identity.
- Not reveal the next or previous ID.
- Follow a separate format for each customer type, with its own prefix and length.
- Be handed out by several service pods at the same time without them waiting on each other.
One pool per customer type
Each customer type has its own pool, and each pool carries its own Feistel seed, prefix and ID length.
Why a Feistel network
A Feistel network turns a counter into a scrambled number of the same size. Because it is a permutation, two different inputs never produce the same output, so the IDs are unique by construction and there is no table to check. Issuing one takes constant time. Nothing is generated ahead of time, and nothing queries the database to see whether an ID is already taken.
// Simplified sketch, not the production code.
long permute(long counter, long domain, long[] roundKeys) {
long x = counter;
do {
x = feistel(x, roundKeys);
} while (x >= domain); // cycle-walk until the result fits the ID length
return x;
}
Claiming without waiting
Pods claim IDs with PostgreSQL FOR UPDATE SKIP LOCKED. When one pod has locked a row, the next pod skips it and takes another, so concurrent onboarding requests do not queue behind each other.
-- Simplified sketch.
SELECT id FROM cif_pool
WHERE customer_type = :type AND status = 'AVAILABLE'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED;
Keeping the pools full
A cron job refills a pool when it drops below a minimum threshold. Each run is capped by a max-batches-per-run setting, so a drained pool cannot send a burst of inserts at the database. A separate reclaim job returns reservations that were left behind when a pod crashed.
Watching it
Micrometer gauges report the depth of every pool, and alerts fire before a pool runs dry.
Tuning the hot table
The allocation table is the busiest one in the flow. Restructuring it cut write amplification, and I dropped a dedup index that had become the throughput ceiling.
Result
Across the pools, the flow issues around 11,000 CIFs a day with no duplicates.
- Java
- Spring Boot
- PostgreSQL
- Micrometer
- Kubernetes
Details are simplified and the client is not named.