Designing Centralised Crypto Exchange
In this article, we will architect and code a micro-service based centralised crypto exchange. The key services of a centralised crypto exchange are trade engine, wallet management, KYC, external API, and UI. The diagram below illustrates the architecture of centralised crypto exchange.
The focus of this article will be on trade engine. Trade engine includes order book, order matching mechanism, order server and order client. The external API includes the API to interface to crypto (eg. BitGo API), and payment gateway.
The UML sequence diagram of the trade engine showing the interaction of the micro-services in trade engine is as below. The order client, order server, order matching are components of trade engine. The redis is used as a queue in this design. The BitGo API is used to interface to crypto (for wallets, sending tokens).
The trade engine is coded in golang. The messaging and services between order client and server are defined using protobuf. The gprc stub is generated by protoc with grpc plugin. The command below can be used to generate stub in golang.
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative simple/simple.proto
Next, we look at some details of the micro services.
The order client calls the grpc services to communicate with the order server.
ret, err := client.SendOrder(ctx, order)
When order is received, the order server stores it in redis database.
err := s.rdb.HSet(ctx, key, “id”, msg.Id, “price”, msg.Price, “qty”, msg.Quantity, “type”, msg.Type, “processed”, false).Err()
Before we can use redis, we must initialise it. In the order server, we define a pointer to redis in server structure.
type teServer struct { pb.UnimplementedTradeEngineServer rdb *redis.Client}
And then we write a initServer function to initialise the server.
func initServer() *teServer { rdb := redis.NewClient(&redis.Options{ Addr: “localhost:6379”, Password: “”, // no password set DB: 0, // use default DB }) s := &teServer{rdb: rdb} return s}
This is done so that the grpc method in order server can obtain access to the rdb.
The initServer function is called as below.
pb.RegisterTradeEngineServer(grpcServer, initServer())
The order matching service creates the orderbook which is defined in trade engine as: (trade engine is a separate package)
type OrderBook struct { BuyOrders []Order SellOrders []Order}
The order matching service loops through the order book. For every order book entry, the code creates an order from reading the corresponding redis entry. The order book processes the order with a call to order book “Process” function.
trades := book.Process(order)
If the order is a buy order, the “Process” function loops through all sell orders against the order. If the order is a sell order, the “Process” function loops through all buy orders against the order.
If the price and quantity match between buy and sell order, a trade entry is created. After order matching service receives the trade entry, the buyer/seller address and quantity of the trade entry is passed to “BitGo api as a service” using a restAPI call.
The figure below is screenshot from order matching service. It shows a trade entry with quantitiy of 100 and price of 10. The API response is received from “BitGo api as a service”.
The “BitGo api as a service” is implemented in nodeJS. It provides the restAPI for order matching service to call. It functions as a middle layer between BitGoAPI and order service. It is responsible for initialising the BitGo sdk, wallet creation, and sending crypto using BitGo sdk.
The source code is available at:
In the next step, the KYC service will be looked into.