Shared Exchange Config & Why Singleton Hurts Tests
Unsynchronized global variables or naive pointer singletons cause race conditions across concurrent trading threads. Worse, public mutable setters (`setMarketOpen(false)`) turn the singleton into an untestable global dependency where concurrent unit tests clobber each other's state, resulting in flaky CI builds.
Naive if (!instance_) instance_ = new ... without synchronization leads to data races where concurrent threads allocate duplicate configuration instances.
Ensure a class has only one instance and provide a global point of access to it, while combining it with an interface and Dependency Injection so unit tests remain fully isolated.
We implement Meyer's Singleton using a static local variable inside instance(), which modern C++ guarantees is thread-safe on first access. We delete copy and move constructors. Crucially, ExchangeConfig implements a pure virtual IExchangeConfig interface, and components take const IExchangeConfig& in their constructors (defaulting to ExchangeConfig::instance()). In unit tests, we inject a lightweight MockExchangeConfig β production keeps its convenient singleton while tests remain 100% isolated.
In Sprint 1 (B1 Β· Builder), we established immutable, fully validated TradeOrder objects. Now that orders can be created safely, our trading engine needs process-wide operational parameters (tick sizes, circuit-breaker price bands, market hours, and order size caps) to govern order validation across all components.
We are building ExchangeConfig, a centralized registry and market clock that provides consistent trading session parameters to RiskEngine, OrderBook, and gateway threads.
Unsynchronized global variables or naive pointer singletons cause race conditions across concurrent trading threads. Worse, public mutable setters (setMarketOpen(false)) turn the singleton into an untestable global dependency where concurrent unit tests clobber each other's state, resulting in flaky CI builds.
By implementing a thread-safe Meyers Singleton implementing an IExchangeConfig interface with constructor dependency injection, production retains safe static access while unit tests inject isolated mock configurations. Next in Sprint 3 (B3 Β· Chain of Responsibility), every incoming TradeOrder (from B1) evaluated against ExchangeConfig (from B2) must pass a sequential pre-trade risk pipeline before reaching the matching engine.
The product ticket
Across our trading system, multiple components (RiskEngine, OrderBook, MatchingGateway, ReportingEngine) require access to the active exchange trading session configuration: ExchangeConfig (market hours, minimum tick size 0.05, and max single-order value cap Rs. 10,000,000).
- β’
Provide a single, globally accessible, thread-safe configuration instance initialized lazily upon first access.
- β’
Prevent accidental duplication, copying, or assignment across the codebase.
- β’
CRITICAL DESIGN REQUIREMENT: Prevent the Singleton from becoming an un-testable global dependency that pollutes test suites with shared mutable state.
A developer implemented ExchangeConfig with public mutable setters (setMarketOpen(bool)). A unit test simulating market closure ran concurrently in CI with order processing tests. Because both tests shared the same global instance, subsequent tests randomly failed, causing flaky builds that took days to debug.
Step 1 β The naive code & why it breaks
The naive code creates a raw pointer singleton with double-checked locking and public mutable setters, and trading components hardcode calls to ExchangeConfig::getInstance() throughout their logic.
#include <print>
#include <string>
// Naive Singleton with raw pointers and public mutable setters.
// Flaw 1: Initialization race condition if two threads call getInstance() concurrently.
// Flaw 2: Mutable global state that leaks side effects across unit tests.
class NaiveExchangeConfig {
private:
static NaiveExchangeConfig* instance_;
bool marketOpen_{true};
double tickSize_{0.05};
double maxOrderValue_{10000000.0}; // Rs. 1 Crore
NaiveExchangeConfig() = default;
public:
static NaiveExchangeConfig* getInstance() {
if (!instance_) { // Race condition without synchronization!
instance_ = new NaiveExchangeConfig();
}
return instance_;
}
bool isMarketOpen() const { return marketOpen_; }
void setMarketOpen(bool open) { marketOpen_ = open; } // MUTABLE GLOBAL STATE!
double tickSize() const { return tickSize_; }
void setTickSize(double t) { tickSize_ = t; }
double maxOrderValue() const { return maxOrderValue_; }
};
NaiveExchangeConfig* NaiveExchangeConfig::instance_ = nullptr;
// A trading component that reaches directly into the global singleton.
class NaiveRiskEngine {
public:
bool canPlaceOrder(double price, int qty) {
auto* cfg = NaiveExchangeConfig::getInstance();
if (!cfg->isMarketOpen()) return false;
if (price * qty > cfg->maxOrderValue()) return false;
return true;
}
};
int main() {
auto* cfg = NaiveExchangeConfig::getInstance();
NaiveRiskEngine risk;
// Normal operation
std::println("Initial: marketOpen={}, canPlace={}",
cfg->isMarketOpen(), risk.canPlaceOrder(2500.0, 100));
// Unit Test 1: Simulating market close by mutating global state
cfg->setMarketOpen(false);
std::println("Test 1 (closed): canPlace={}", risk.canPlaceOrder(2500.0, 100));
// Unit Test 2: Expects default open market, but FAILS because Test 1 polluted the singleton!
std::println("Test 2 (polluted!): canPlace={}", risk.canPlaceOrder(2500.0, 100));
}Naive if (!instance_) instance_ = new ... without synchronization leads to data races where concurrent threads allocate duplicate configuration instances.
When Test A calls cfg->setMarketOpen(false), Test B (which expects the default open market) fails intermittently. Unit tests lose isolation and cannot run in parallel.
RiskEngine directly reaches out to the global ExchangeConfig::getInstance(). Callers cannot test edge cases (such as custom tick sizes or foreign exchange limits) without modifying production global state.
Step 2 β The fix: Singleton Pattern (and its Testing Cost)
Ensure a class has only one instance and provide a global point of access to it, while combining it with an interface and Dependency Injection so unit tests remain fully isolated.
We implement Meyer's Singleton using a static local variable inside instance(), which modern C++ guarantees is thread-safe on first access. We delete copy and move constructors. Crucially, ExchangeConfig implements a pure virtual IExchangeConfig interface, and components take const IExchangeConfig& in their constructors (defaulting to ExchangeConfig::instance()). In unit tests, we inject a lightweight MockExchangeConfig β production keeps its convenient singleton while tests remain 100% isolated.
IExchangeConfig β declares const inspection methods (isMarketOpen, tickSize, maxOrderValue, exchangeName).
ExchangeConfig β implements IExchangeConfig with a private constructor, deleted copy/move, and thread-safe static local instance().
MockExchangeConfig β implements IExchangeConfig for isolated test scenarios without touching global state.
RiskEngine β takes const IExchangeConfig& via constructor injection.
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.