Fintech· 10 min read

Lessons from Building Payment Systems at Scale

At Rappi, I was part of the small team that bootstrapped RappiPay from zero — including Colombia's first co-branded credit card with Davivienda and a digital savings account that grew to 100K+ active users. Here are the hard lessons.

Money Is Not Just a Number

The first mistake many engineers make when building a payment system is representing money as a floating-point number. float amount = 10.50 — this will haunt you. Floating-point arithmetic is not exact. In payments, fractions of a cent matter at scale.

The rule: always store monetary amounts in the smallest indivisible unit (cents, in most cases), as an integer. 1050 for $10.50. Do all arithmetic in integers. Format for display only at the presentation layer.

// ❌ Dangerous
const amount: number = 10.50;

// ✅ Safe  
const amountCents: number = 1050; // always integer, always cents

The Ledger is Sacred

Every payment system is, at its core, an accounting system. The golden rule: every debit has a matching credit — the ledger must always balance. We enforced this at the database level with triggers and at the application level with reconciliation jobs that ran every 15 minutes.

When we integrated the Davivienda credit card, we discovered that the bank's API returned transaction statuses asynchronously — sometimes hours later. Our reconciliation system had to account for pending, authorized, settled, and reversed states, each with corresponding ledger entries.

⚠️ Lesson Learned

Never trust a payment provider's webhook as the source of truth. Build reconciliation jobs that query their API independently and compare against your internal state.

Idempotency: Do Not Skip This

Mobile networks are unreliable. Users tap "Pay" and get a spinner. They tap again. Your server receives the request twice. Without idempotency, you charge them twice.

Every mutating API endpoint in RappiPay required an Idempotency-Key header. On the server we stored (user_id, idempotency_key) → response with a 24-hour TTL. This pattern eliminated duplicate transactions entirely.

Regulatory Compliance is an Engineering Problem

Financial regulation in LATAM varies by country. In Colombia, the Superintendencia Financiera sets rules around KYC, transaction limits, and AML screening. These have direct engineering implications:

  • KYC data must be encrypted at rest and in transit, with strict access controls
  • Transaction records must be immutable — append-only audit tables, no UPDATE or DELETE
  • AML screening requires external watchlist APIs with sub-second SLA requirements
  • Data residency requirements may forbid certain cloud regions

The Co-Branded Card Launch

Launching Colombia's first co-branded credit card between Rappi and Davivienda required integrating with the bank's decades-old SOAP-based core banking system. We built a dedicated adapter service — a strangler fig pattern — that translated all interactions. When Davivienda upgraded their systems a year later, we only had to update the adapter.

What I Wish I Had Known

  1. Design error states explicitly — A payment can succeed, fail, or be in an unknown state. Handle all three.
  2. PCI-DSS is a journey, not a checkbox — Start the compliance process early. The audit takes months.
  3. Invest in a testing sandbox early — Mirrors production, saves enormous debugging time.
  4. Observability = sleep — The teams that sleep are the ones with dashboards, not the ones paged at 3am.

José Alejandro Berrío MarínLead Software Engineer · 9+ years in fintech across LATAM · ex-Mercado Libre, Rappi, Leal