How We Helped a Fintech Startup Overcome the Event Sourcing Trap in a Microservices Migration
Everyone talks about microservices like they’re magic. Break the monolith, sprinkle some events, and suddenly you’re Netflix. Reality is messier.
A few months back, a fintech startup in Singapore came to us with a specific problem. They’d just migrated their core transaction engine from a Rails monolith to event-driven microservices. The architecture looked beautiful on the whiteboard.
Why Your Open Source Project Needs a Formal RFC Process (And How to Implement One on GitHub)
Why Your Open Source Project Needs a Formal RFC Process (And How to Implement One on GitHub) You’ve… ...
But in production? Their read models were stale. Transactions that completed 15 seconds ago still showed as “pending” in the UI. Users were refreshing five times. Customer support was drowning in “where’s my money?” tickets.
Here’s the kicker: they’d already hired two different offshore teams to build this. Both failed. One team tried to patch the sync logic. The other blamed Kafka.
Multi-Agent Systems: Why Your Orchestration Is Probably Wrong (And How to Fix It)
Multi-Agent Systems: Why Your Orchestration Is Probably Wrong (And How to Fix It) I’ve reviewed over thirty multi-agent… ...
We walked in, looked at their event sourcing pipeline, and saw the real problem immediately. It wasn’t Kafka. It wasn’t their tech stack. It was an architectural anti-pattern that most teams never catch until it’s too late.
The Root Cause: The Projection Trap
Their system worked like this:
Transaction Service → Kafka → Projection Service → PostgreSQL Read Model
When a transaction happened, the Transaction Service published an event. The Projection Service consumed it and updated a read model table. Simple enough.
But here’s where it broke: multiple microservices were emitting events that affected the same read model. The Wallet Service, the Fraud Service, and the Ledger Service all published events that updated the same `account_balance` projection. These services had no shared ordering. No coordination.
So events arrived out of order. A `DebitConfirmed` event would land *before* the `DebitInitiated` event. The Projection Service would calculate a negative balance or skip the update entirely. Eventually, the read model would drift by 10-15 seconds. Under load, it hit 45 seconds of lag.
Honestly, this pattern is so common it has a name: the eventual consistency trap. Everyone says “eventual” is fine. But try telling that to a fintech user who can’t see their transfer go through.
We needed an immediate fix, not a theoretical one. The client had a Series A to close in six weeks.
The Fix: A State Machine Projector with Idempotent Updates
Instead of rewriting their whole pipeline, we built a single state machine projector that sat between Kafka and the PostgreSQL read model. It was a small, focused service written by our senior engineers in Can Tho.
Here’s the core code pattern we used to enforce event ordering and idempotency:
python
import json
from datetime import datetime
from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
import asyncpg
# The state machine defines valid transitions
TRANSITIONS = {
"DebitInitiated": {"next_states": ["DebitConfirmed", "DebitFailed"]},
"DebitConfirmed": {"next_states": ["Settled"]},
"DebitFailed": {"next_states": ["Refunded", "RetryInitiated"]},
"Settled": {"next_states": []}, # terminal state
}
async def apply_event(conn, account_id, event_type, payload):
# 1. Check current state
current_state = await conn.fetchval(
"SELECT state FROM account_state WHERE account_id = $1 FOR UPDATE",
account_id
)
# 2. Validate transition
allowed = TRANSITIONS.get(current_state, {}).get("next_states", [])
if event_type not in allowed:
# Reject outdated event, log and skip
return False, f"Invalid transition: {current_state} -> {event_type}"
# 3. Idempotent update using event_id as dedup key
result = await conn.execute("""
INSERT INTO account_state (account_id, state, balance, version, updated_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (account_id, event_id) DO NOTHING
""", account_id, event_type, payload["balance"], payload["version"], datetime.utcnow())
return result == "INSERT 0 1", "Applied"
This was not complex. It was just correct. Three key decisions made this work:
- State machine enforcement: Events that arrived out of order were rejected, not applied. We tracked the current state per account.
- Pessimistic locking with FOR UPDATE: No two instances of the projector could process the same account concurrently. This prevented double-spends.
- Event ID as dedup key: Even if Kafka redelivered the same event, our `ON CONFLICT DO NOTHING` rule meant it wouldn’t corrupt the read model.
The junior engineers on our team handled the rest—alerting, monitoring, and a small dashboard to visualize lag. The middle engineers focused on performance tuning the PostgreSQL connection pool. Seniors owned the state machine logic.
The Results: Stale Data Dropped by 94%
We deployed this projector in three days. Yes, three days. Here are the metrics from production two weeks later:
| Metric | Before | After | Improvement |
|---|---|---|---|
| Read model lag (p95) | 14.2 seconds | 0.8 seconds | 94% reduction |
| Failed projections per hour | 187 | 3 | 98% reduction |
| Support tickets for balance issues | 42/day | 2/day | 95% reduction |
| Projection service CPU usage | 78% | 23% | 70% less |
But here’s the stat that mattered to the CEO: zero data inconsistencies in the first month post-deploy. The read model and the transaction log were perfectly synchronized for the first time since the migration.
We didn’t use any fancy tech. We used a Vietnamese team, an 80-line state machine, and the ECOA AI platform to accelerate code generation and test coverage. The AI orchestration layer (ECOA ACP) handled the boilerplate: consumer setup, retry logic, health checks.
The team achieved this at a fraction of the cost their previous offshore partners charged. Senior developers at $3,000/month, middle at $2,000/month. Total cost for this engagement was under $15,000 across 5 weeks.
Why the Previous Teams Failed
This is worth unpacking because it’s a pattern I see everywhere.
The first offshore team was a body-shop. They knew Kafka basics but had zero domain knowledge about fintech transaction flows. They just wrote generic consumers that applied events blindly. No state machine. No validation.
The second team was more technical but over-engineered. They tried to build a distributed saga manager with compensating transactions. It looked impressive on paper. In practice, it introduced so much complexity that the system became unmaintainable within a month.
Our team in Can Tho took a different approach. We asked one question: What is the simplest thing that makes this correct?
The answer was a state machine. Not a saga. Not an orchestrator. Just a deterministic set of rules that said “this event can only happen after this event.”
More importantly, we had the senior experience to say no to complexity. That’s what you get when you hire Vietnamese developers who are vetted for senior-level thinking, not just coding speed.
Lessons Learned for Your Next Migration
If you’re migrating a monolith to microservices, especially in fintech, here’s what I’d tell you:
Don’t trust eventual consistency for money flows. Build a state machine into your projection layer from day one. It takes two days to write and saves months of debugging.
Test with real event ordering chaos. Your dev environment uses sequential, in-order events. Production doesn’t. Introduce random delays in your test pipeline. You’ll catch the weirdest bugs.
Idempotency is not optional. Every event handler must be a pure function of the event ID. If you can’t run it twice safely, you haven’t built it correctly.
Use a smaller, smarter team. Our 3-person team (1 senior, 1 middle, 1 junior) outperformed the previous 12-person teams. Quality of thinking beats quantity of engineers every time.
Actually, this last point is why I believe in the Vietnam model. You’re not paying for hours. You’re paying for judgment. And $3,000/month for a senior developer who has seen 10 production outages and knows how to avoid the 11th? That’s the best ROI you’ll find.
Final Thoughts
This case study isn’t about AI replacing engineers. It’s about AI-augmented engineers who know when to use a blunt tool and when to use a scalpel. The state machine projector was the scalpel.
If you’re stuck on a microservices migration, or your event sourcing pipeline is leaking state, give us a shout. We’ve seen every version of this problem. And our team in Vietnam knows how to fix it without the hype.
—
Frequently Asked Questions
What programming languages did your team use for the projector?
We used Python with `aiokafka` and `asyncpg`. The client’s existing stack was Python-centric, so it fit naturally. We could have used Go for lower latency, but the difference wasn’t meaningful for their throughput (200 events/second). Python kept the maintenance burden low for their in-house team.
How did the ECOA AI platform contribute to this project?
The ECOA AI Platform ACP accelerated boilerplate code: consumer setup, retry logic, circuit breakers, and health check endpoints. Our senior developers used AI-assisted code generation for the projection logic, but validated every line manually. It cut development time by roughly 40% without sacrificing quality.
Can this pattern work for non-fintech applications?
Absolutely. Any system with ordered state transitions benefits from this: e-commerce order flows, inventory management, logistics tracking. The core idea—a state machine with idempotent event handlers—applies anywhere you need to trust your read model.
How long did it take to fully onboard your team to the client’s codebase?
Our senior engineer was productive by day two. The middle engineer needed about four days to understand the full event flow. We scheduled a half-day knowledge transfer session with the client’s lead architect. After that, we only needed Slack async communication. That’s the advantage of hiring senior-heavy teams: they ramp fast.
Related reading: Hire Vietnamese Developers: Why Vietnam Tech Talent Is Your Smartest Move in 2025
Related reading: Why Vietnam Outsourcing Is the Smartest Offshoring Move for 2025