
Speed and simplicity rarely come together. In pursuit of one we more often than not have to sacrifice the other. Yet, all these stock exchanges somehow manage sub millisecond latency, for millions?!
This treasure hunt led me to make farzi.exchange.
farzi (फ़र्ज़ी) translates to "fake" in Hindi, which is precisely what this is. The goal was to build a fake stock exchange, ground up from first principles, while trying to retain a decent amount of performance without overengineering, or overspending for compute.
A couple of resources proved to be very useful while building this:
While both of these are skewed towards the US markets, the very core of a financial exchange never changes.
So then, that's enough introduction. Let's get into what an exchange truly is.
What makes a Financial Exchange?
the bombay stock exchange, mumbai
The core purpose of a financial exchange is to match orders, primarily buy and sell orders, between buyers and sellers (who wouldve thought?!). Financial Exchanges work best when they are trade fungible instruments, i.e. one unit is economically indistinguishable from all other units.
This is done via maintaining an Order Book, which is updated thousands to millions of times every second.
The core of an Order Book is a data structure that holds all the buy and sell orders in memory, usually partitioned on an instrument level.
Then a Matching Algorithm refers to this Order Book, to match orders that go hand in hand and make trades out of them.
This algorithm has a list of rules it swears by, to make sure all matches are deterministic, reproducible and most importantly, fair.
Taking inspiration from NSE, farzi.exchange is a price-time exchange, which adheres to the rule of Orders are matched based on price and time, with the best bid/ask that arrived the earliest, matched first. For tie breaks, exchanges usually adopt different parameters to tip one order over the another, usually these are:
- Disclosed Quantity (the more you disclose, the higher you shall stand)
- Order Size (can go either way, with exchanges preferring larger orders over smaller ones and vice versa)
Now that we have valued one order over another, what different shapes can an order take?
Order Type Mayhem
An order is a trader's way of communicating with the exchange, their wants and more importantly, their terms.
Modern exchanges support a variety of order types, each serving a different purpose and scenario.
Limit Orders
Limit Orders are one of the most common order types, which is basically a traders way of telling the exchange "I am willing to Buy/Sell XYZ, but only if its under/atleast this price."
This gives a trader the illusion of being in control.
Market Orders
Another common order type present in almost all exchanges are Market Orders, which are used to execute a trade at the current best market price, no strings attached. Do note that between the time an order is placed on the client and it reaches the exchange, the price may have changed.
While usually this difference isnt much, not having a Limit Order can be a problem if the market price moves against you significantly.
Another con of Market Orders is they fill against the Best Bid/Ask opposite to you, thus making you eat the spread.
The spread is the difference between the Best Bid and Best Ask prices, basically the gap between the lowest someone is willing to sell an instrument at and the highest someone is willing to buy it at.
Iceberg Orders
An iceberg order is a trading technique that splits a large buy or sell order into smaller, sequential limit orders to conceal the true trade size.
Only a small visible portion (the "tip of the iceberg") hits the order book, while the rest remains hidden. As each visible leg executes, the next automatically replenishes.
an example iceberg order
The reason large traders prefer iceberg orders is it hides a trader’s true market intentions from competitors, stopping front-running and other manipulative trading behaviors.
Exchanges such as NSE do have a restriction of a minimum of 10% of the total order value to be disclosed at the time of order placement.
That's a bunch of trading lingo, but it is essential to know them to better understand some key decisions. Moving on to the technical part of the Exchange, its design.
The Exchange Architecture
On a higher level, the exchange ended up looking something like this:
farzi.exchange architecture
That's a lot of blocks. Each one of them have their dedicated role as part of the exchange and we will shortly go over them in detail and as to what problem they solve.
The Exchange would then initialize everything in a back-to-front order, building them off of each other, to ensure each component has all its dependencies initialized prior to its initialization.
There's a bunch of key moving parts, let's go over them one by one and see what they are and more importantly, why they are.
The Matching Engine
This is the heart and soul of any exchange. It holds the Order Book in memory and for each order it recieves, it tries matching it immediately with the best Bid/Ask on the opposite side. If it is able to match it adhering to Exchange rules, it makes a trade out of it. Else it pushes it to the back of the queue for execution later.
I decided to design the engine in a way that I was able to run parallel goroutines for each unique instrument my exchange served, which is a fair assumption to make in such a setup, although some exchanges don't do this because of reasons beyond the scope of what I was trying to achieve.
The matchmaking flow looks like this:
the farzi.exchange matching engine
Hence the Matching Engine initialization inside the Exchange looked something like this:
func NewExchange() (*Exchange, error) {
// Get all the instruments that are part of the exchange
instruments, err := repo.ListInstruments(context.Background())
// Make engines for all the instruments
e := &Exchange{
engines: make(map[string]*Engine),
Events: make(chan Event, eventsBuffer),
wg: sync.WaitGroup{},
...
}
// Initialize other parts of the exchange here ...
// Finally make goroutines for each engine
for _, engine := range e.engines {
e.wg.Add(1)
go func(engine *Engine) {
engine.Run()
}(engine)
}
return e, nil
}Each matching engine has its own in memory orderbook for its specific symbol, which looked a bit like:
type Engine struct {
book *book.OrderBook // Its own in memory orderbook
cmds chan Command // Commands that the Engine recieves
events chan<- Event // Events that the Engine emits, to be consumed by the aggregators
...
}Now, for each command the exchange recieves for a symbol, we just use a simple Routing Map to route it to its specific engine!
That looked a bit like:
func (e *Exchange) Route(cmd Command) {
// Get the specific engine for this incoming command's symbol
engine := e.engines[cmd.Symbol]
select {
// send the command to the engine's command channel
case engine.cmds <- cmd:
}
}The thing with the engine is, it has to do this, for millions of orders, every waking minute of the session. This is where the design of the Order Book plays a key role.
The Order Book
It's responsibility is to manage and update thousands of orders, at insane speeds. Thus this demands for storage to be in memory, catering to the speed and availability requirements.
a typical orderbook
It has to be optimized for quick lookups, finding the best price possible and checking if price levels exist already or not for insertion of incoming orders.
This can be as simple as a 2D array of prices and orders, to something complex. But with simplicity you sometimes lose speed.
In the end, I decided to go forward with using a Binary Tree for representing the Bid and Ask Order Books. This was partly because the tick indexing decisions weren't final which would be the base for tick indexed arrays, and could change depending on how the engine is used.
The setup finally looked like this:
the orderbook data structure
Essentially, we have a binary tree made up of nodes of PriceLevel , where each PriceLevel has the Volume at that price and a pointer to a list of Orders that are at the node's price.
Most of everything else is made to aid frequently performed actions on the tree, by making maps for O(1) indexed lookups.
That translated to code that looked like:
type PriceLevel struct {
Price fixed.Fixed // The price of this level
Orders *list.List // Doubly linked list of all the orders at this price level
Volume fixed.Fixed // The total volume at this price level, used for OB Snapshots for O(1)
}
type BookSide struct {
Levels *btree.BTreeG[*PriceLevel] // All the levels inside the btree
LevelByPrice map[fixed.Fixed]*PriceLevel // For O(1) lookups of whether a price level exists already or not
BestLevel *PriceLevel // Cache the best PriceLevel of the side for faster retrievals
Better func(a, b *PriceLevel) bool // Just a comparision function, same as less() used in BTree at NewBookSide
}
type BookEntry struct {
level *PriceLevel // The price level this entry belongs to.
elem *list.Element // The actual order in the linked list on that level
}
type OrderBook struct {
Symbol string // OrderBooks are scoped to an instrument, ideally to be run in separate goroutines.
Bids *BookSide
Asks *BookSide
OrderIndex map[uint64]*BookEntry // For O(1) lookups of orders for cancellations
}Now that we have matched orders and made trades out of them, what next? How does a trade affect an instrument's price and more importantly, how does the trader know?
The Emitter
We now have the trades, but how do we let other components know? A lot of things need to reflect a trade's success. The instrument price must go up if the price the trade was executed at is higher than the current price, the clients who placed the trade need to know about it, all people watching the stock need to know!
For this we have our emitter. It is responsible for fanning out events to multiple consumers, depending on the event type. It looks a bit like:
the emitter
func (e *Exchange) runEmitter() {
for ev := range e.Events {
switch ev.Kind {
case EventTrade:
// On getting a Trade Event, send it on to all the interested consumer's sinks
e.tradeSink <- ev.Trade
e.aggregatorTrades <- ev.Trade
...
case EventSnapshot:
// On getting a periodic Order Depth Snapshot Event, send it on to market data consumer for letting listeners know!
e.quoteBuilder.IngestSnapshot(ev.Snapshot)
case EventOrder:
// On getting an Order, let the order sink consumer know for persistence!
e.orderSink <- ev.Order
}
}
}The Aggregator
The primary job of the aggregator is to eat up events emitted by the engines, and make candles out of it.
That looked a bit like:
type Aggregator struct {
aggregatorTrades <-chan *types.Trade // the input channel it gets events from the engine
aggregatorCandles chan<- *types.Candle // the output channel it pushes made candles to
aggregatorDone chan struct{} // the done channel is closed when the aggregator is done
// the live candle state in memory, per symbol. A 2D map of [interval][symbol] which has
// the candle at that position in the matrix.
live map[types.Interval]map[string]*types.Candle
...
}It makes candles in memory, to be pushed to live websocket listeners, and then periodically also sends it to get stored in our database to aid the historical data API.
But how? How does anything, get stored in our database? Wait, we have a database?! Wasn't everything in memory!
The Persistence Layer
While storing stuff in memory is incredibly fast, its expensive and volatile. For orders, trades, historical data, we can't afford to lose any of it. Thus we have our very handy plug-and-play Persistence Layer.
The persistence repository is exposed as part of the Exchange itself and any component can call it and it's helpers to help store data into the postgres database via the persistence layer.
But since writes are usually blocking, we have to be careful not to block the main exchange loop. Thus we make use of our good ol goroutine buffered channel approach to store batched data asynchronously.
Around ~22G of data is generated by market makers every trading session, which is retained into postgres and cleared periodically.
The MarketData Layer
The market data layer is responsible for serving market data to clients via websockets, and also providing historical data via the API. It listens for events from the Aggregator and the Emitter, for sending out updated Candles and OrderBook depth snapshots.
All reads go to the repository (persistence layer), different from the writes that get routed via the exchange. This CQRS seperation allows us to serve reads scaled independently without blocking the main exchange loop.
The MarkerData layer runs a websocket hub where clients can connect and subscribe to instrument updates, and a router goroutine sends out non blocking messages to all interested clients, as the events are received!
This hub looks a bit like this:
the market data hub
This translates to code that looks like this:
type Hub struct {
register chan *Client
unregister chan *Client
subs chan subReq
broadcast chan broadcastMsg
stop chan struct{}
clients map[*Client]struct{}
subsBySymbol map[string]map[*Client]struct{} // symbol -> subscribed clients
...
}We see that we have a handy routing table for outgoing messages, and a set of clients connected to the hub.
That's almost all of the Exchange architecture! Now that we know what happens when you place an order behind the scenes, who's actually on the other side?
Who's gonna fill my order?
Each exchange strives to be liquid. This means at any point, any trader should be able to place an order and have it filled as soon as possible, from another trader on the other side of the book, with minimal delay.
Since farzi.exchange doesn't have a lot of active traders (who wouldve guessed?!), I decided to make my own persona based Market Makers.
A market maker is a financial firm or individual that actively quotes both a buy (bid) and a sell (ask) price for an asset, standing ready to trade at all times.
But in a market only comprised of symmetric market makers, prices will always stay stagnant. Thus I decided to spice things up a little, and gave them personas.
I added primarily 4 types of Market Makers:

Farzi Maker
Two-sided quoter
Bread-and-butter market maker. Posts a ladder of bids and asks around the mid and skews it against its inventory to stay flat. Strictly passive - adds top-of-book liquidity without pushing price.
Farzi Seth
Value investor
Deep-pocketed and patient. Anchors to fair value and lays large, wide walls that lean against deviations - bidding dips, offering spikes. Provides depth, damps volatility, and seeds an empty book.
Farzi Scalper
Spread tightener
Works tiny size at a fast cadence, right at the touch, never crossing. Keeps spreads narrow and the tape ticking with frequent small fills - instruments feel liquid and candles come out clean.
Farzi Gambler
Noise trader
The one persona that moves price. Carries a directional bias and occasionally takes liquidity to push the tape. Bounded by a tight position cap, a fair-value band, and small size - life, not chaos.
Adding these market makers to the exchange, the instruments started reflecting with real-time order book depth and candlestick data, with changing prices!
instruments on farzi.exchange
The Farzi 10 Index
Each good exchange has its own indexes to value growth of the market at. Indian Exchanges have the SENSEX and NIFTY 50, US exchanges have the S&P 500, we have the FARZI 10 Index.
the farzi 10 index
A standard way of computing an index of an exchange from its underlying instruments is as follows:
Essentially, if the value of an underlying instrument increases, so does the index's value.
We have the Index Calculator that listens for events from the Emitter out of the Matching engine and computes the index value accordingly on each trade, sampled at 100ms.
Where is this running?
As of when I write this, the client runs on Vercel (honestly any place is fine, Vercel just seemed easier to deploy on) and the exchange is hosted on an Ubuntu m7i-flex.large box on AWS. The same machine hosts the API, the market makers and the database with 200 gigs on the EBS volume, all powered by the free 200$ worth of credits.
The Big Leagues are calling
On 15th July 2026, farzi.exchange was able to process a total of 2,81,35,609 trades and a total of 12,21,15,996 orders!

For context, the most amount of trades recorded by NSE in a single day was 24,25,13,525, on Jun 19, 2024. That's only about 8.6x our daily volume! Given the current compute and setup, not too shabby eh?
What's Next?
Honestly, I'm not sure. One thing I have gained out of this is an immense amount of respect for exchanges as a whole. Managing multiple market types, commodity, futures, options, and more, does certainly sound like a lot of work.
Thanks for readin! Now you know most of what happens when you place an order on the farzi.exchange :)
