Every fintech app development tutorial on the internet teaches you the same thing: connect Stripe, build a wallet screen, show a transaction list. Ship it.
Then you work on a real fintech product — one that regulators can audit and real users trust with real money — and you discover that the tutorial covered maybe 15% of the actual work. The other 85% is the stuff nobody writes about because it's unglamorous, compliance-heavy, and learned through painful production incidents.
After several years working on fintech app development projects — wallets, lending platforms, payment systems — here's the stuff I wish someone had told me on day one.
- KYC is a workflow, not an API call
Tutorials treat KYC like this:
javascriptconst result = await kycProvider.verify(userDocument);
if (result.verified) activateAccount(user);
Reality: KYC is a state machine with a dozen states, and your architecture needs to treat it that way.
A real verification flow looks more like: PENDING → DOCUMENTS_SUBMITTED → AUTO_REVIEW → MANUAL_REVIEW → APPROVED / REJECTED / RESUBMISSION_REQUIRED — and users will exist in every one of these states simultaneously across your user base. Each state changes what the user can do: maybe they can browse in PENDING, deposit small amounts in AUTO_REVIEW, but can't withdraw until APPROVED.
Things that will bite you if you don't plan for them:
Providers time out and fail. Onfido, Jumio, Sumsub — they all have outages and slow days. Your onboarding can't hard-block on a synchronous verification call. Build it webhook-driven, with the user parked in an intermediate state.
Manual review is unavoidable. Some percentage of users will always fall into manual review (blurry documents, name mismatches, sanctions list near-matches). You need an internal admin tool for compliance officers on day one — not "later."
Re-verification exists. KYC isn't one-and-done. Regulators expect periodic re-verification and event-triggered checks (address change, unusual activity). Your data model needs verification records, plural, per user — not a boolean is_verified column. I've seen that boolean column in production. It did not end well.
Tiered KYC is a product decision with legal consequences. Many markets allow limited functionality with light verification (phone + ID number) and full functionality after document verification. Your limits engine and your KYC states have to be designed together.
The lesson: model KYC as an event-driven state machine from the start. Retrofitting it into an app built around if (user.verified) is a multi-month rewrite.
- PCI DSS: the goal is to never touch card data
The single most important PCI DSS lesson: compliance scope is determined by where card data flows, and your job is to make sure it never flows through you.
If raw PANs (card numbers) touch your servers — even in memory, even in logs — you're in for the full PCI DSS assessment: quarterly scans, network segmentation, key management ceremonies, the works. That's a six-figure ongoing burden.
The sane architecture:
User's browser/app → (card data) → Payment provider's hosted fields/SDK
↓
Your backend ← (opaque token) ← Provider tokenizes the card
Your backend only ever stores and transmits tokens. Stripe Elements, Braintree Hosted Fields, Adyen components — they all exist precisely so card data goes straight from the user's device to the provider, bypassing your infrastructure entirely. This drops you into SAQ-A territory, the lightest compliance tier.
The mistakes I've actually seen in code reviews:
Logging request bodies globally. A generic request logger that captures everything — including the one endpoint where a card number passes through. Congratulations, you now have PANs in your log aggregator, and your log aggregator is now in PCI scope.
"Temporary" card storage. Someone stores card details "just for the retry flow." There is no temporary in compliance.
Card data in error reports. Your crash reporting tool captures local variables. One of those variables was a payment payload. Sentry is now in scope.
Practical rule we enforce in every fintech app development project: add a serialization deny-list for fields like pan, cvv, card_number at the logging middleware level, so even a careless log statement can't leak them.
- Your transaction table is not a ledger
Tutorial code:
sqlUPDATE wallets SET balance = balance - 100 WHERE user_id = 42;
If you take one thing from this post: never store balance as a mutable number. A real fintech backend uses double-entry ledger design — every movement of money is an immutable pair of entries (debit one account, credit another), and balance is the derived sum of entries.
Why this matters beyond accounting purity:
Auditability. When a user disputes a balance ("I had ₹5,000 yesterday!"), you can replay every entry. With a mutable balance column, you're archaeology-deep in application logs praying you logged enough.
Reconciliation. Every fintech product eventually reconciles against external systems (bank statements, processor reports). Reconciliation against an append-only ledger is a diff. Reconciliation against mutated balances is a nightmare.
Concurrency. Two simultaneous withdrawals against a mutable balance is the classic race condition that creates negative balances. Append-only entries with database-level constraints handle this far more safely.
And the companion lesson: idempotency keys on every money-moving endpoint. Mobile networks retry. Users double-tap. Webhooks re-deliver. Without idempotency keys, every retry is a potential double-charge:
javascriptPOST /transfers
Idempotency-Key: 7f9c2e-...
// Server: if this key was seen before, return the original
// response — do NOT execute the transfer again.
Payment providers (Stripe et al.) support this natively. Your internal APIs need it too.
- Webhooks will lie to you (about ordering)
Fintech backends are webhook-driven: payment succeeded, KYC completed, transfer settled. Two things tutorials never mention:
Webhooks arrive out of order. You will receive payment.settled before payment.created. Your handlers must be order-independent — treat each webhook as "here's the current state of object X, go fetch the full object and reconcile," not as a sequential event stream.
Webhooks arrive more than once. Same event, delivered three times. Every handler must be idempotent (notice a theme?).
And verify webhook signatures. Always. An unverified webhook endpoint is an open invitation to mark arbitrary payments as "completed."
- Compliance shapes architecture — involve it early
The biggest non-technical lesson: in fintech app development, compliance is not a checklist you run before launch. It's an architectural stakeholder.
Data residency requirements decide your cloud regions. AML rules decide your transaction monitoring pipeline. Audit requirements decide your logging retention (and its immutability). Regulator reporting decides your data warehouse schema. Discovering any of these after the architecture is set means expensive rework.
On the projects that went smoothly, compliance requirements were in the BRD before the first sprint. On the ones that didn't... well, that's how I learned everything in this post. This is honestly the strongest argument for working with teams that have shipped regulated products before — at Dev Technosys, where I've collaborated on several fintech builds, discovery phases now start with a compliance mapping session before any tech stack discussion, because retrofitting compliance is always more expensive than designing for it.
The honest summary
Fintech app development is 15% features and 85% correctness: state machines instead of booleans, ledgers instead of balance columns, tokens instead of card data, idempotency everywhere, and compliance in the room from day one.
None of this is glamorous. All of it is what separates apps that survive their first audit — and their first traffic spike, and their first dispute — from apps that make the news for the wrong reasons.
What did your first fintech project teach you the hard way? Drop it in the comments — I'm collecting scars.
Top comments (0)