Loading home…
Loading home…
Loading article…
Six boxes on the diagram. Six microservice temptations. I almost fell for it again — until I got that the full map doesn't mean shipping it all in week one, and splitting repos doesn't split the domain

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.

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.

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.
In the previous article I ended with an idea that still works as a compass: the full map does not mean shipping the full map in week one. What it does mean is not dumping everything in a controller. And there's a trap in the middle: thinking that because you split repos you split the domain.
Spoiler: not always.
In arkano-banking-challenge I did the usual thing when you're against the clock: one service that does everything and a controller that knows it. It worked for delivery. Not so much for explaining.
Rebuilding in fintech-core-platform, the other extreme stared at me from the README: microservices on day one, enterprise diagram, impressive LinkedIn screenshot. Pretty in a capture. Heavy to operate when it's you alone at the keyboard with no distributed tracing wired up.
The middle ground that convinces me now is boring to say on stage but honest to maintain: a modular monolith. One artifact. One PostgreSQL at the start. Modules that don't import diagonally and domain that doesn't know NestJS.
That's not giving up on microservices. It's not paying their bill early.
I don't like posts that draw hexagonal with ten empty boxes. In practice I think about four things:
Domain says what's allowed: you don't suspend a closed account, you don't invent rules in the controller. Application coordinates: open account, find by id. Infrastructure is PostgreSQL today, another adapter tomorrow. Interface is HTTP, DTOs, input validation.
NestJS lives in interface and infra. If you see @Injectable or TypeORM inside domain/, something broke.
A microservice is another binary with another release cycle. A modular monolith is the same process with internal walls. When Transfers arrives, I want it to be a module that respects those walls — not a copy-paste of the Arkano controller.
Today I care about local ACID, stack traces that don't hop between pods, and testing Account.suspend() without Docker. Tomorrow — if the product and team ask for it — I'll care about deploying Transfers without touching Accounts.
| Now (Phase 0–1) | When splitting makes sense |
|---|---|
| One deploy, local transactions | Two teams that can't coordinate releases |
| In-process calls | One context needs 10× scale, another doesn't |
| One backlog, few people | Clear data boundaries, no magic cross-service joins |
Move money across databases without a consistency model and you're back to the timeout question from the previous post — except the failure is distributed. I'm not there yet. I'm trying to get accounts modeled properly before arguing about pods.
Phase 0 isn't a slide. It's on GitHub with this shape:
apps/accounts-service/
├── accounts/
│ ├── domain/ # no @nestjs, no typeorm
│ ├── application/
│ ├── infrastructure/
│ └── interface/
libs/domain-common/
The Account entity lives in domain and enforces rules — not a DTO with getters:
export class Account {
private constructor(
readonly id: AccountId,
readonly ownerName: string,
private _status: AccountStatus,
readonly createdAt: Date,
) {}
static open(id: AccountId, ownerName: string): Account {
return new Account(id, ownerName, AccountStatus.ACTIVE, new Date());
}
suspend(): void {
if (this._status === AccountStatus.CLOSED) {
throw new AccountClosedError(this.id);
}
this._status = AccountStatus.SUSPENDED;
}
}
The use case talks to a port, not TypeORM:
export class CreateAccountUseCase {
constructor(private readonly repo: AccountRepositoryPort) {}
async execute(command: CreateAccountCommand): Promise<AccountId> {
const id = AccountId.generate();
const account = Account.open(id, command.ownerName);
await this.repo.save(account);
return id;
}
}
And the controller — as promised when we talked about POST /transfer — only passes data and returns a response:
@Post()
@HttpCode(HttpStatus.CREATED)
async create(@Body() dto: CreateAccountDto): Promise<AccountResponseDto> {
const id = await this.createAccount.execute({
ownerName: dto.ownerName,
});
return AccountResponseDto.fromId(id);
}
pnpm test runs domain and application tests without a database. Swagger at /api/docs. Docker Compose brings up PostgreSQL on port 5433. No Kafka yet. No fraud. Good.
"When would you split Accounts and Transfers?"
I don't start with Kubernetes. I look for two teams that need to ship without waiting on each other, a context that needs far more scale than another, traces and event contracts that amortize distributed pain. Without two clear signals, I stay with strong modules in the same deploy.
The ports you write today help tomorrow: the use case shouldn't care if the adapter is TypeORM or a message on a bus.
Transfers and ledger come in later phases in the same binary until the domain says otherwise. Next article is money in code: cents, bigint, and why float makes me uneasy in a banking core.
In the previous post I drew the layers behind POST /transfer. Here I chose where to put the code while I learn: one building, ordered departments, one deploy for now.
Series: makingcode.dev/series/fintech-core-platform
Previous: Behind POST /transfer
Code: github.com/AndresED/fintech-core-platform