Building an Exchange with Claude Code (3) — Throwing Java Out for Go + Rust
Third post in the exchange series. In part 1 and part 2 I built an exchange on Java/Spring. This one is about tearing that down and rebuilding it.
Why rebuild
The Spring Boot exchange worked fine. But two things kept nagging at me in operation.
Memory footprint. Running 17 microservices means the JVM heaps alone add up fast. ECS Fargate billing scales with memory, and at 512MB–1GB per service it was more than I wanted to carry personally.
Matching engine latency. Execution logic should not have GC pauses. You can tune your way there in Java, but I kept coming back to the thought: “for this part specifically, I want a language with no GC at all.”
So I decided: 13 API services in Go, 2 matching engines (spot and futures) in Rust. I did not delete the legacy Java code — I left it in the backend/ folder as a migration reference. Being able to check “wait, what was the original logic here?” while writing new code turned out to help far more than I expected.
The Rust matching engine
The heart of it is the order book. Expressing price-time priority as a data structure looks like this:
- Bids:
BTreeMap<Reverse<Decimal>, VecDeque<Order>>— highest price first - Asks:
BTreeMap<Decimal, VecDeque<Order>>— lowest price first - Within a price level,
VecDequegives you FIFO
Every monetary value goes through rust_decimal. Do money math in floating point and you will, eventually and without fail, have an incident.
The futures engine was far hairier than spot: liquidation, funding rate, TP/SL, position management — each file is a state machine hundreds of lines long. Honestly, without Claude Code this part would have taken months. The division of labor worked well: I’d use five years of exchange domain knowledge to pin down exactly “this is the liquidation price formula, and here are the edge cases,” and the AI would fill in implementation and tests quickly.
One design decision I enjoyed
The matching engine has to be a singleton. Two of them running at once scrambles execution ordering. The usual answer is a distributed lock in code; I enforced it in infrastructure instead.
Setting deployment_minimum_healthy_percent = 0 in Terraform makes ECS kill the existing task before starting the new one during a deploy. You get a brief outage, but there is never a moment when two engines coexist. Orders are sitting durably in Kafka, so a few seconds of downtime costs nothing. Knowing which problems to solve in code and which to solve in infrastructure — that might be what experience actually buys you.
Results
- 13 Go services + 2 Rust engines, 48,000+ lines together
- 102 MySQL tables, a Kafka execution pipeline, STOMP real-time market data
- Deployed to AWS ECS Fargate via Terraform; memory usage felt like a third of the Java version
- Validated by running it as a live demo for a while (it’s down now due to AWS costs — I learned everything I wanted to from building it, so no regrets)
“Re-platformed while keeping the legacy around” is a more accurate description than “built from scratch.” And re-platforming is much harder, because you’re constrained to preserve every existing behavior. In the next post I’ll cover the gap-analysis documents and verification approach I built along the way.