← Projects

Naoto Exchange

Personal R&D, Jul 2025-present

A trading exchange I built alone: it takes orders, matches buyers with sellers and keeps every balance up to date, pairing an order in under a microsecond.

C++20DPDKSSE2/SSE4epoll (ET)lock-free SPSCetcd
Source on GitHub ↗
101.03
12
101.02
19
101.01
28
101.00
42
100.99
42
100.98
28
100.97
19
100.96
12
600ns
to match an order (P99)
1.1µs
to confirm it to the client (P99)
40k/s
orders per second
0
memory allocations per order

An exchange is where buyers and sellers meet. Someone offers to buy at a price, someone else offers to sell, and the exchange pairs them up and records who now owns what. Professional exchanges do this millions of times a day, and the time each order takes matters, because whoever gets matched first gets the price.

Naoto is one I've been building alone since mid-2025, to find out how that kind of system stays fast without ever getting a balance wrong. It pairs an order in about 600 nanoseconds, well under a thousandth of a millisecond, and it keeps running while parts of it are replaced.

etcd · leases, watchesClientsOrder gatewayepoll → risk → routeclient state: replicaMatching engineone per asset, one threadsource of truthMarket datasubscribersAccount servicebalances: replicaordersacksTCP or shm SPSClogin +snapshottrade reports, seq-numberedDPDK multicastbook updates
Solid: the order path. Dashed: everything kept off it.

It's three kinds of service. Clients connect to an order gateway, which checks that they can afford what they're asking for. Each asset has its own matching engine, which pairs buy and sell orders. An account service owns every balance. Services can join and leave the cluster at runtime, and the rest adapts without a restart.

Why it's fast

Before any code was written, every step an order sets off was designed from two sides at once: what the rest of the system actually needs from it, and what the hardware does when it runs. The aim was to keep as little as possible on the hot path, the part an order is actually waiting on.

Most of the work could move off it. Some moved earlier. Memory is set aside when the system starts, and a client's balance is fetched from the account service once, at login, instead of on every order. Asking another machine anything costs a network round trip, and on the scale an order works at, that would dwarf everything else it does.

THE FIRST 1.2 µs, ZOOMED INmatch an orderP99600 nsconfirm it to the clientP991.1 µs0400 ns800 ns1.2 µsan order, end to endone round trip to another machinetypically tens of µs010 µs20 µs30 µs40 µs50 µs
One round trip to another machine costs more than an order's whole trip through Naoto.

The rest moved sideways, onto threads running next to the hot one: sending the confirmation back, applying trades to balances, broadcasting results. An order only waits on a few short steps (moving work off the hot path goes through each of them).

Every piece of data also has exactly one owner. Two parts of the system never write the same thing, so they never have to coordinate, which removes most of the locks a system like this would normally need and keeps processor cores from fighting over the same memory (one writer for every piece of state).

What couldn't move was made fast where it is. The order book, where prices and pending orders live, keeps two structures side by side, one for finding a price and one for knowing which price is best (one order book, two data structures). The first is a hand-built hash map that uses SIMD instructions to check sixteen slots at once (the SIMD key-value store).

Why it stays correct

Speed is only useful if the numbers are right. The risk check has to decide instantly whether a client can afford an order, using information that is always slightly behind, and it does so by reserving the funds the moment it accepts (reliable risk checking).

Every trade report carries a sequence number, so a service that falls behind can catch up exactly, with nothing skipped or applied twice (catching up a stale snapshot).

When a service disappears and its connection is replaced, the gateway notices if an order was in flight and refuses to confirm it rather than risk it having gone to the wrong place (when a descriptor changes mid-send).

Where it stands

The core path, from order to match to balance update, works end to end, with latency measured at every stage. Two pieces are still being built: a retransmission service that recovers trade reports lost on the network, and a stricter ordering for closing old connections so a stale one can never be reused mid-send.