Orders With Optional Limit, Stop, Validity & Iceberg Qty
A 9-argument telescoping constructor (`TradeOrder("RELIANCE", BUY, LIMIT, 100, 2850.5, 0.0, true, false, 20)`) causes traders to swap `limitPrice` and `stopPrice` or construct contradictory orders (e.g. a `MARKET` order with a limit price).
When adjacent parameters share the same type (int qty and int displayIcebergQty, or double limitPrice and double stopPrice), the compiler cannot detect when a developer inverts their order at the call site.
Separate the construction of a complex object from its representation, allowing step-by-step configuration and enforcing cross-field invariants before creating an immutable product.
We hide Order's constructor and expose an OrderBuilder. Mandatory fields (symbol, side, qty) are required upfront in the builder's constructor. Fluent, self-describing methods (withLimitPrice(), withIceberg()) configure optional attributes. Finally, build() validates all cross-field rules (e.g. iceberg <= qty, limitPrice > 0) and returns a std::expected<Order, std::string>. Once built, Order has no setters and remains completely immutable.
Day 1 of building our low-latency Trading Platform (Zerodha / Binance style). Before we can route, risk-check, or match orders, we need a rock-solid TradeOrder domain object representing Market, Limit, Stop-Loss, and Iceberg orders.
We are designing the TradeOrder creation API used by trader UIs and algorithmic strategies.
A 9-argument telescoping constructor (TradeOrder("RELIANCE", BUY, LIMIT, 100, 2850.5, 0.0, true, false, 20)) causes traders to swap limitPrice and stopPrice or construct contradictory orders (e.g. a MARKET order with a limit price).
With TradeOrder::Builder enforcing cross-field invariants before creating an immutable TradeOrder, we now have valid orders. Next in Sprint 2 (B2 · Singleton), we need a single process-wide ExchangeConfig & MarketClock registry so all trading threads read consistent tick sizes and circuit-breaker limits.
The product ticket
Our trading terminal handles high-speed equity orders. An Order has mandatory core parameters (symbol, side Buy/Sell, total quantity) alongside multiple optional execution parameters: limitPrice (for Limit orders), stopPrice (for Stop-Loss triggers), timeInForce (DAY, IOC, FOK), displayIcebergQty (hidden liquidity where only a slice is visible on the public order book), and a client routing tag.
- •
Allow constructing orders with any valid combination of optional parameters without a combinatorial explosion of constructors.
- •
Validate domain invariants before the object is created: quantity must be positive, limit prices cannot be negative, and
displayIcebergQtymust never exceed total quantity. - •
Ensure constructed
Orderobjects are strictly immutable so they can be safely passed to concurrent matching engines and risk checks without lock contention.
A trading desk deployed a hotfix with an 8-parameter constructor Order("RELIANCE", Side::Buy, 100, 2450.0, 0.0, DAY, 1000, "DESK_1"). The developer accidentally swapped total quantity (100) and iceberg quantity (1000), sending an invalid order straight to the exchange. Furthermore, callers can construct invalid half-built orders with limit prices set to zero.
Step 1 — The naive code & why it breaks
The naive implementation relies on telescoping constructors with default arguments, or a parameter-heavy constructor that forces callers to pass dummy values for unused optional fields.
#include <print>
#include <string>
enum class Side { Buy, Sell };
enum class TimeInForce { DAY, IOC, FOK };
// Naive Order with an 8-parameter telescoping constructor.
// Multiple parameters of the same type lead to catastrophic swapped-argument bugs.
class NaiveOrder {
public:
std::string symbol;
Side side;
int qty;
double limitPrice;
double stopPrice;
TimeInForce tif;
int displayIcebergQty;
std::string clientTag;
NaiveOrder(std::string sym, Side s, int q, double lPrice = 0.0,
double sPrice = 0.0, TimeInForce t = TimeInForce::DAY,
int iceberg = 0, std::string tag = "")
: symbol(std::move(sym)), side(s), qty(q), limitPrice(lPrice),
stopPrice(sPrice), tif(t), displayIcebergQty(iceberg), clientTag(std::move(tag)) {}
};
int main() {
// BUG 1: Trader meant qty=1000, iceberg=100, but swapped int parameters at call site!
// The compiler silently accepts this, causing an unwanted 100-share iceberg with 1000 visible!
NaiveOrder badOrder1("RELIANCE", Side::Buy, 100, 2450.0, 0.0, TimeInForce::DAY, 1000, "DESK_1");
std::println("Bad Order 1: qty={}, iceberg={}", badOrder1.qty, badOrder1.displayIcebergQty);
// BUG 2: Limit order intended, but limitPrice omitted and defaulted to 0.0!
// No validation ensures limitPrice > 0 for non-market orders.
NaiveOrder badOrder2("INFY", Side::Sell, 50);
std::println("Bad Order 2: symbol={}, limitPrice={}", badOrder2.symbol, badOrder2.limitPrice);
}When adjacent parameters share the same type (int qty and int displayIcebergQty, or double limitPrice and double stopPrice), the compiler cannot detect when a developer inverts their order at the call site.
Using mutable setters (setLimitPrice, setQty) leaves the object in a partially configured, invalid state until all setters are invoked, exposing incomplete state to other threads.
Supporting combinations of Limit, Stop, Iceberg, and IOC requires dozens of overloaded constructor signatures that become impossible to maintain.
Step 2 — The fix: Builder Pattern
Separate the construction of a complex object from its representation, allowing step-by-step configuration and enforcing cross-field invariants before creating an immutable product.
We hide Order's constructor and expose an OrderBuilder. Mandatory fields (symbol, side, qty) are required upfront in the builder's constructor. Fluent, self-describing methods (withLimitPrice(), withIceberg()) configure optional attributes. Finally, build() validates all cross-field rules (e.g. iceberg <= qty, limitPrice > 0) and returns a std::expected<Order, std::string>. Once built, Order has no setters and remains completely immutable.
Order — the immutable object containing validated trading fields, with private constructors and public const accessors.
OrderBuilder — collects parameters fluently, checks business invariants in build(), and constructs the Order.
Trading strategies and desk algorithms that configure orders with readable method chaining.
Unlock Full UML Diagram & C++23 Solution
The interactive UML architecture diagrams, refactored C++23 code, real-world extensions, and interview Q&A are part of Brute2Best Pro. All 7 foundation chapters are 100% free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.