Microservices vs Monoliths: What We Learned Building Trading Systems
Practical insights on when to use microservices and when a well-structured monolith is the better choice
The microservices vs monolith debate often generates more heat than light. After building both architectures for high-stakes trading systems, we've learned that the answer is nuanced. This post shares our real-world experience.
Microservices offer compelling benefits:
What the tutorials don't tell you:
Instead of one system to monitor, you have dozens:
1# Suddenly you need all of this:
2services:
3 - order-service (3 instances)
4 - execution-service (2 instances)
5 - risk-service (4 instances)
6 - market-data-service (5 instances)
7 - position-service (2 instances)
8 - analytics-service (3 instances)
9 - notification-service (2 instances)
10
11monitoring:
12 - Prometheus (metrics)
13 - Grafana (dashboards)
14 - Jaeger (distributed tracing)
15 - ELK Stack (logging)
16 - PagerDuty (alerting)
17
18infrastructure:
19 - Kubernetes cluster
20 - Service mesh (Istio/Linkerd)
21 - Message broker (Kafka/RabbitMQ)
22 - API gateway
23 - Load balancers
24Real cost: 3 DevOps engineers full-time vs 0.5 for a monolith.
Every service boundary adds latency:
1// Monolith: 50μs
2function processOrder(order: Order): Result {
3 const risk = calculateRisk(order); // function call
4 const position = getPosition(order); // function call
5 return executeOrder(order, risk); // function call
6}
7
8// Microservices: 5000μs
9async function processOrder(order: Order): Promise<Result> {
10 const risk = await riskService.calculate(order); // HTTP: 1500μs
11 const position = await positionService.get(order); // HTTP: 1500μs
12 return await executionService.execute(order, risk); // HTTP: 2000μs
13}
14100x latency increase - unacceptable for HFT systems.
A simple database transaction becomes a distributed saga:
1// Monolith: Simple transaction
2async function transferFunds(from: Account, to: Account, amount: number) {
3 await db.transaction(async (tx) => {
4 await tx.debit(from, amount);
5 await tx.credit(to, amount);
6 });
7}
8
9// Microservices: Saga pattern
10async function transferFunds(from: Account, to: Account, amount: number) {
11 const sagaId = generateId();
12
13 try {
14 // Step 1: Debit
15 await accountService.debit(from, amount, sagaId);
16
17 // Step 2: Credit
18 await accountService.credit(to, amount, sagaId);
19
20 // Step 3: Confirm
21 await sagaService.complete(sagaId);
22 } catch (error) {
23 // Compensating transactions
24 await accountService.rollback(sagaId);
25 throw error;
26 }
27}
28What could go wrong:
Each service has its own database, leading to synchronization challenges:
1// Order Service DB
2{
3 orderId: "123",
4 symbol: "AAPL",
5 quantity: 100,
6 status: "FILLED"
7}
8
9// Position Service DB
10{
11 symbol: "AAPL",
12 quantity: 95 // Out of sync!
13}
14
15// Risk Service DB
16{
17 symbol: "AAPL",
18 exposure: 100 * 150 // Using stale quantity!
19}
20Despite the challenges, microservices are the right choice when:
Different business capabilities with minimal interaction:
1┌─────────────────┐
2│ Trading │
3│ Platform │
4└─────────────────┘
5 ↓
6┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
7│ Market Data │ │ Reporting │ │ Compliance │
8│ (real-time) │ │ (batch) │ │ (async) │
9└─────────────────┘ └─────────────────┘ └─────────────────┘
10
11These can be separate microservices - minimal interaction
121Market Data Service: [====] [====] [====] [====] [====] (5 instances)
2Risk Service: [====] [====] [====] [====] (4 instances)
3Order Service: [====] [====] (2 instances)
4Analytics: [====] (1 instance)
5Large organization with independent teams:
1Team A: Market Data (10 people, real-time specialists)
2Team B: Risk Engine (8 people, quantitative analysts)
3Team C: Order Management (6 people, trading domain experts)
4Team D: Analytics (5 people, data scientists)
5For HFT systems, we use a modular monolith:
1// Single process, but modular
2class TradingEngine {
3private:
4 MarketDataModule market_data_;
5 RiskModule risk_;
6 OrderModule orders_;
7 ExecutionModule execution_;
8
9public:
10 void on_market_data(const Quote& quote) {
11 // In-memory, zero-copy: 0.5μs
12 auto risk = risk_.check(quote);
13
14 if (risk.allow) {
15 // Direct function call: 0.2μs
16 auto order = orders_.generate(quote, risk);
17
18 // Inline execution: 0.3μs
19 execution_.send(order);
20 }
21 // Total: 1μs vs 5000μs for microservices
22 }
23};
24Team size < 20 people - coordination overhead of microservices exceeds benefits.
Startups and MVPs benefit from monolith simplicity:
1// Monolith: One repo, one deployment
2git push
3npm run build
4npm run deploy // Done in 5 minutes
5
6// Microservices: Update 3 services
7cd order-service && git push && deploy
8cd risk-service && git push && deploy
9cd execution-service && git push && deploy
10// Configure service mesh, update routing, test integration
11// Done in 2 hours (if nothing breaks)
12Our recommended approach for most financial systems:
1// Logical modules with clear boundaries
2src/
3 modules/
4 market-data/
5 domain/
6 models.ts
7 events.ts
8 application/
9 service.ts
10 infrastructure/
11 repository.ts
12
13 risk/
14 domain/
15 application/
16 infrastructure/
17
18 orders/
19 domain/
20 application/
21 infrastructure/
22
23// Enforced dependencies
24{
25 "rules": {
26 "market-data": ["risk"], // market-data can depend on risk
27 "orders": ["risk", "market-data"], // orders depends on both
28 "risk": [] // risk has no dependencies
29 }
30}
31✅ Low latency - function calls, not HTTP
✅ Simple deployment - single binary
✅ ACID transactions - real database transactions
✅ Easy debugging - single process to trace
✅ Future-proof - modules can be extracted to services later
If you must migrate monolith → microservices:
1// Before: Spaghetti
2function processOrder(order) {
3 // 500 lines mixing everything
4}
5
6// After: Modules
7function processOrder(order) {
8 const risk = RiskModule.calculate(order);
9 const position = PositionModule.get(order);
10 return ExecutionModule.execute(order, risk);
11}
12Start with services that:
1Monolith Microservices
2├── Market Data ───────> Market Data Service (real-time)
3├── Risk
4├── Orders
5├── Execution
6└── Analytics ───────> Analytics Service (batch)
7└── Reporting ───────> Reporting Service (batch)
8Only extract core services if:
Initial architecture: Pure microservices (2019)
115 microservices
25ms average latency
33 DevOps engineers
4$50k/month infrastructure
5Frequent outages from cascading failures
6Current architecture: Modular monolith (2024)
11 core trading monolith
23 separate services (analytics, reporting, compliance)
3200μs average latency (25x improvement)
41 DevOps engineer
5$8k/month infrastructure
6Zero outages in 18 months
7Results:
Use microservices when:
Use a modular monolith when:
The truth: Most systems benefit from a well-structured monolith with optional microservices for genuinely independent capabilities.
Don't let architectural trends drive your decisions. Let your actual requirements (latency, team size, domain complexity) guide you.
At NordVarg, we help clients:
Contact us to discuss your architecture challenges.
Technical Writer
NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.
Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.