Before writing any code we spent a while on one question: which language makes this particular exchange easiest to get right, to prove right, and to explain? We were not looking for the best language in general. We were looking for the one whose defaults fail loudly on the mistakes an exchange cannot afford.
What the exchange punishes
- Numbers that are not exact. Every price, quantity and balance is a whole number that fits in 64 bits. A price of
10.5must be rejected, not quietly rounded. - Errors in the wrong order. If a request has no valid token and a broken body, the answer must be 401, not 400. The order of checks is part of the contract.
- Races. Two orders reaching for the same cash, a cancel racing a fill.
- Slow recovery. The server is killed repeatedly while traders keep working.
What Go gives us
- Strict numbers by default. Decoding JSON into an
int64rejects10.5,"10"and1e2without any extra code. - Our own order of checks. Login first, then parsing, then lookups, then business rules, in plain code. No framework decides it for us.
- A natural shape for the design. One goroutine per asset, receiving commands over a channel, is the standard Go way to have a single writer. And
go test -racechecks every test run for data races. - Fast restarts. The server is back in milliseconds after a crash.
- One binary. Nothing to install next to it, so it runs the same on any server.
Where the others lost
Python with FastAPI is the fastest to write, and its tools for property-based testing are excellent. But FastAPI parses the JSON body before it runs authentication, so a broken body without a token gets a 422 instead of a 401. Its default number handling also accepts "10" and true as integers. Each of these can be configured away, but each one is a trap you have to know about.
TypeScript on Node would give us one language for everything. But every JSON number in JavaScript is a 64-bit float, so integers above 2^53 are rounded without a word. In a system whose first rule is “never create or lose money”, that is a risk we did not want to manage everywhere.
Java with Spring Boot is the industry’s default for backends, and that counts. But its JSON library accepts 10.5 as 10 by default, the MCP library supports the current protocol only in a pre-release, and the framework does a lot of work out of sight. We want every line of our code to be easy to explain, and hidden behaviour makes that harder.
Rust would rule out data races at compile time, but learning it well takes weeks we would rather spend on the exchange itself.
C# came closest. It is strict and fast, with good tooling. It is also the language our team knows least, and that decided it.
What Go costs us
Go is not free of traps either. Integer overflow wraps around silently, so we check every price × quantity ourselves, with a test. An empty list encodes as null unless we create it explicitly. There are no sum types, so a linter checks that every switch over order sides and statuses handles every case. And error handling is verbose.
We chose these costs on purpose. Each of them is visible in the code, and each has a test or a linter rule that keeps it in check.
