Loading home…
Loading home…
Loading article…
Three seconds on the spinner. Timeout on screen. Did the money move? I reopened my Arkano challenge next to a fintech architecture map — and understood why POST /transfer was never just an endpoint.

No spam — just a note when I publish something new on backend, cloud, and architecture.
One email when a post goes live. Unsubscribe anytime.

A LinkedIn post on fintech architecture, a recent technical challenge, and a decision to rebuild a banking core from scratch — slower this time, to actually learn.

If onboarding a new customer requires deploying a new application or duplicating an entire database, your SaaS architecture probably isn't ready to scale. Learn how to implement multi-tenancy in NestJS in a clean and maintainable way.

If every new feature forces you to modify five different services, you probably have a coupling problem. Learn how Event-Driven Architecture helps decouple modules and scale NestJS applications.
Three seconds on the spinner. Blank screen. The request took too long.
The user is not thinking about PostgreSQL or idempotency keys. They are thinking about whether their money came back or left for good. And you, on the other side, need to know.
When that happened to me for the first time not in production, in a demo I could not answer. The endpoint had returned 200 before. But this time the connection dropped before the client read the response. Had it transferred or not?
That question is the thread of this article.
In the series introduction I explained why I am rebuilding a banking core from zero, slowly. The promise was specific: not another sprint against the clock, but understanding and explaining each layer.
The first step was not opening NestJS. It was reopening old decisions.
When I started this series, I reopened arkano-banking-challenge with a fintech architecture map in another tab gateway, auth, accounts, fraud, Kafka, observability. I scrolled to transfers and stared at a method I had written a few months earlier.
It worked. I had passed the challenge. But next to that map, my POST /transfer told a different story someone who optimized for Postman's green check, not for the timeout question.
I say that without shame. A 48-hour challenge rewards delivery. It does not reward explaining what happens when something fails halfway through.
On the user's screen, everything fits in four gestures:
Four steps. Absolute simplicity. That is product: design, copy, the feeling that "this is easy."
In Arkano, I had designed for step 4. I had barely thought about the eight steps that can live between the tap and the confirmation in a fintech that already scales.
One afternoon, with a fintech architecture sketch on one screen and a notebook on the other, I tried to translate each box into a concrete question. This is what came out:
| Step | Component | What it solves | Sync or async? |
|---|---|---|---|
| 1 | API Gateway | Who gets in and at what pace? (TLS, rate limit, WAF) | Sync |
| 2 | Auth | Is this person allowed to do this? (JWT, scopes, MFA) | Sync |
| 3 | Accounts | Is there balance, an active account, limits? | Sync |
| 4 | Transfers / Ledger | Does money move without breaking invariants? | Sync (DB TX) |
| 5 | Fraud | Is this pattern suspicious? | Sync or async |
| 6 | Outbox + bus | Who else needs to know? | Async |
| 7 | Notifications | How do we tell the user? | Async |
| 8 | Observability | Can we reconstruct what happened? | Async / sidecar |
The first time I read the full table I thought: all of this to move $100 from one account to another?
The second time I understood something many fintech diagrams repeat without saying out loud: complexity does not disappear. It only moves. The user does not see it because someone at some point decided where each concern lives. Product hides complexity; engineering places it.
And here is the nuance that took me years to internalize: none of these steps is "fluff" in production, but not all of them exist in week one. Knowing the full map and building only what the current phase needs that is the craft. Not YAGNI versus the diagram; YAGNI with the diagram on the wall.
System design interviews ask for something like this. In production, every arrow has an owner, SLA, and runbook. I did not have it when I wrote my first endpoint only the urgency to make it compile:
Today I look at it and split two paths in my head.
The path the user waits for auth, balance, move money, response. If this takes more than a few seconds, anxiety kicks in.
The path that can wait email, push, metrics, async audit. If the bus is slow, the transfer is already done; the user should not find out through an infinite spinner.
In Arkano I mixed both in the same method. Email rode the request path. The transfer depended on SMTP. I did not see that on challenge day; I saw it the day I compared my code to the diagram.
This is what my endpoint looked like:
@Post('transfers')
async transfer(@Body() dto: TransferDto) {
const from = await this.repo.findOne(dto.fromAccountId);
if (from.balance < dto.amount) throw new BadRequestException();
from.balance -= dto.amount;
const to = await this.repo.findOne(dto.toAccountId);
to.balance += dto.amount;
await this.repo.save([from, to]);
await this.emailService.sendReceipt(dto.userEmail);
return { ok: true };
}
I remember the satisfaction of seeing it green in Postman. Account A down, account B up, email sent, ok: true. Demo done. Mental snapshot saved.
Months later, the same block became a list of questions I could not answer calmly:
balance, what happens to the penny lost in rounding?save fails halfway, what is on disk without an explicit transaction?The snippet is not wrong for a time-boxed challenge. It is incomplete for the craft I want to practice now. And all those questions converge on one scene.
Back to the start: the user taps Send. The spinner runs. Three seconds. Timeout.
Did the money move?
With balance -= amount and return { ok: true }, you do not know. Maybe you debited and the client never read the 200. Maybe it failed before save and the user thinks they sent. Maybe they retried and you charged twice.
In fintech, "I don't know" is not an acceptable answer. Not for support. Not for a senior interview. Not for sleeping well if this ever touches real money.
What I needed and what I will build in this series comes down to three ideas:
Idempotency-Key returns the same response with no double debit.PENDING | COMPLETED | FAILED, not just a silently mutated balance.That is why the ledger shows up in Phase 1. Not microservices posturing. Because without those three pieces, the timeout question still has no answer.
When I only wanted to ship fast, the trade-off confused me. I thought there were two options: full enterprise diagram or CRUD and done.
Over time I understood there is a third the one this series tries to show:
| Approach | What you gain | What you risk |
|---|---|---|
| Full diagram day 1 | Impressive README | Shallow understanding |
| CRUD only, no map | Fast delivery | Going blank in interviews |
| Full map + incremental build | Honest learning | Discipline not to skip phases |
Hidden complexity vs YAGNI is not picking a side. It is keeping the map on the wall and putting only today's bricks on the table.
For Phase 0 of fintech-core-platform that means:
Idempotency-Key from the first real endpoint, OpenAPI as the contract.In Arkano I prioritized the green check. Here I prioritize being able to explain what happens on every arrow in the diagram starting with the ones the user never sees.
This is the code I want to write now: thin controller, logic in the use case, no email or Kafka on the path the user waits for:
@Post()
@HttpCode(HttpStatus.CREATED)
async create(
@Headers('idempotency-key') idempotencyKey: string,
@Body() dto: CreateTransferDto,
): Promise<TransferResponseDto> {
const result = await this.createTransfer.execute({
idempotencyKey,
fromAccountId: dto.fromAccountId,
toAccountId: dto.toAccountId,
amountCents: dto.amountCents,
});
return TransferResponseDto.from(result);
}
The use case orchestrates domain + persistence port. Unit tests without Docker. One brick not the whole building.
The building fills in phase by phase: ledger in phase 1, events in phase 2, fraud when the sync flow no longer lies. No shortcuts. No README promising what the code cannot yet support.
Same domain as Arkano. Different pace. In the intro I said I ran once; now I am walking the marathon. This article is the first kilometer the one that starts when you stop looking only at the green check and start looking at the map.
Series: makingcode.dev/series/fintech-core-platform
Previous: Why I'm rebuilding a fintech core