
How to Build a Simple Order-Matching Algorithm in Java
In trading markets, the order-matching algorithm is the core mechanism behind every order execution. Whenever buyers and sellers submit orders, the system automatically matches and executes them according to the rules of price priority and time priority…
In trading markets, the order-matching algorithm is the core mechanism behind every order execution. Whenever buyers and sellers submit orders, the system automatically matches and executes them according to the rules of price priority and time priority. These rules are familiar to experienced stock investors, but from a developer's perspective, designing an efficient and reusable matching engine remains a challenge. This article explains how to build a simplified order-matching algorithm with Java from a technical implementation perspective.
Design Approach for the Bid and Ask Sides
The core of the matching engine is the OrderBook, which maintains two queues: the bid side and the ask side. The bid side is sorted from highest to lowest price, ensuring that the buy order with the highest quote is executed first; the ask side is sorted from lowest to highest price, ensuring that the sell order with the lowest quote is executed first.
In a trading market, the buy1 price can never be greater than or equal to the sell1 price; otherwise, it indicates that the system has missed an order that should have been executed. For multiple orders at the same price, execution follows time order—in our implementation, a globally unique sequenceId ensures the sorting and processing order instead of using the creation time directly.
Many people might think of using List<Order> to store orders, but in a high-frequency trading environment, the cost of inserting and deleting orders is too high (O(N)), making efficiency difficult to guarantee. A better approach is to use a balanced tree structure, such as Java's TreeMap, which keeps the time complexity of insertion, deletion, and lookup operations at O(logN).
public record OrderKey(long sequenceId, BigDecimal price) { }
Because the bid and ask sides have different sorting rules, two different comparators must be provided for TreeMap:
private static final Comparator<OrderKey> SORT_SELL = (o1, o2) -> {
int cmp = o1.price().compareTo(o2.price()); // Lower prices first
return cmp == 0 ? Long.compare(o1.sequenceId(), o2.sequenceId()) : cmp;
};
private static final Comparator<OrderKey> SORT_BUY = (o1, o2) -> {
int cmp = o2.price().compareTo(o1.price()); // Higher prices first
return cmp == 0 ? Long.compare(o1.sequenceId(), o2.sequenceId()) : cmp;
};
With the comparator in place, implementing OrderBook is very straightforward:
public class OrderBook {
public final Direction direction;
public final TreeMap<OrderKey, OrderEntity> book;
public OrderBook(Direction direction) {
this.direction = direction;
this.book = new TreeMap<>(direction == Direction.BUY ? SORT_BUY : SORT_SELL);
}
public OrderEntity getFirst() {
return this.book.isEmpty() ? null : this.book.firstEntry().getValue();
}
public boolean remove(OrderEntity order) {
return this.book.remove(new OrderKey(order.sequenceId, order.price)) != null;
}
public boolean add(OrderEntity order) {
return this.book.put(new OrderKey(order.sequenceId, order.price), order) == null;
}
}
Tip: In Java, always use compareTo() to compare BigDecimal values, not equals(); otherwise, 1.2 and 1.20 will be considered unequal.
Core Logic of the Matching Engine
With the buy and sell order books, you can implement a matching engine. The core data structure is as follows:
public class MatchEngine {
public final OrderBook buyBook = new OrderBook(Direction.BUY);
public final OrderBook sellBook = new OrderBook(Direction.SELL);
public BigDecimal marketPrice = BigDecimal.ZERO;
private long sequenceId;
public MatchResult processOrder(long sequenceId, OrderEntity order) {
switch (order.direction) {
case BUY:
return processOrder(order, this.sellBook, this.buyBook);
case SELL:
return processOrder(order, this.buyBook, this.sellBook);
default:
throw new IllegalArgumentException(“Invalid direction.”);
}
}
}
When processing an order, if it is a buy order, it attempts to match with the sell order book; if it is a sell order, it attempts to match with the buy order book. The new order is called Taker, orders already placed on the book are called Maker. If a Taker is not fully filled, it becomes a Maker and is placed on the order book.
MatchResult processOrder(OrderEntity takerOrder, OrderBook makerBook, OrderBook anotherBook) {
long ts = takerOrder.createdAt;
MatchResult matchResult = new MatchResult(takerOrder);
BigDecimal takerUnfilledQuantity = takerOrder.quantity;
for (;;) {
OrderEntity makerOrder = makerBook.getFirst();
if (makerOrder == null) break;
if ((takerOrder.direction == Direction.BUY && takerOrder.price.compareTo(makerOrder.price) < 0) ||
(takerOrder.direction == Direction.SELL && takerOrder.price.compareTo(makerOrder.price) > 0)) {
break;
}
this.marketPrice = makerOrder.price;
BigDecimal matchedQuantity = takerUnfilledQuantity.min(makerOrder.unfilledQuantity);
matchResult.add(makerOrder.price, matchedQuantity, makerOrder);
takerUnfilledQuantity = takerUnfilledQuantity.subtract(matchedQuantity);
BigDecimal makerUnfilledQuantity = makerOrder.unfilledQuantity.subtract(matchedQuantity);
if (makerUnfilledQuantity.signum() == 0) {
makerOrder.updateOrder(makerUnfilledQuantity, OrderStatus.FULLY_FILLED, ts);
makerBook.remove(makerOrder);
} else {
makerOrder.updateOrder(makerUnfilledQuantity, OrderStatus.PARTIAL_FILLED, ts);
}
if (takerUnfilledQuantity.signum() == 0) {
takerOrder.updateOrder(takerUnfilledQuantity, OrderStatus.FULLY_FILLED, ts);
break;
}
}
if (takerUnfilledQuantity.signum() > 0) {
takerOrder.updateOrder(takerUnfilledQuantity,
takerUnfilledQuantity.compareTo(takerOrder.quantity) == 0 ? OrderStatus.PENDING : OrderStatus.PARTIAL_FILLED,
ts);
anotherBook.add(takerOrder);
}
return matchResult;
}
MatchResult records the current Taker order and all matching records, making it convenient for the settlement system to perform settlement.
Supports multiple trading pairs
One engine instance can process only one trading pair. To support multiple trading pairs in the same system, you can use an engine group to manage them:
class MatchEngineGroup {
Map<Long, MatchEngine> engines = new HashMap<>();
public MatchResult processOrder(long sequenceId, OrderEntity order) {
Long symbolId = order.symbolId;
MatchEngine engine = engines.get(symbolId);
if (engine == null) {
engine = new MatchEngine();
engines.put(symbolId, engine);
}
return engine.processOrder(sequenceId, order);
}
}
Adding a symbolId property to each order routes it to the corresponding trading pair engine instance, keeping the system internally isolated.
Example Demonstration
Suppose the following orders are submitted to the matching engine:
SidePriceQuantitybuy2082.341sell2087.62buy2087.81buy2085.015sell2088.023sell2087.606buy2081.117buy2086.03buy2088.331sell2086.542sell2086.555buy2086.553
After matching is completed, the buy and sell orders are automatically updated, and the latest market transaction price changes accordingly. The system's internal state is fully reproducible. Every transaction strictly follows the principles of price priority and time priority, making the entire matching process clear and efficient.