Trading systems operate at a level of reliability most software never has to reach. A social media app can go down for five minutes and users will refresh. A trading platform that fails for five seconds can mean millions in losses, regulatory penalties, and reputational damage that takes years to repair.
At a global investment bank, I worked on post-trade systems processing hundreds of millions of pounds in daily transactions. These systems handled complex financial instruments like interest rate swaps, required strict regulatory compliance (EMIR, MiFID II), and demanded 99.99% uptime with sub-second response times. This post covers the architecture decisions that made that possible.
What you will learn: Event-driven microservices architecture for fintech · Trade compression engine design · Real-time risk calculation at scale · Database optimization and intelligent batching · Regulatory compliance automation (EMIR, MiFID II) · Performance results: 75% faster processing, 62% less memory, 95% fewer queries
1. The Domain: Post-Trade Processing
When two banks execute a trade, the work is not done. What follows - post-trade processing - is a complex chain of confirmations, calculations, settlements, and regulatory reports. At a global investment bank, this meant:
- Trade compression: Identifying and netting offsetting interest rate swap positions to reduce capital requirements.
- Settlement: Confirming trades with counterparties and managing the actual transfer of funds.
- Risk calculation: Computing real-time exposure across all positions using distributed computing.
- Regulatory reporting: Generating EMIR and MiFID II reports with automated audit trails.
Diagram 1: Post-trade processing lifecycle
2. Architecture: Event-Driven Microservices
The core of the platform was an event-driven microservices architecture. Each stage of the pipeline was an independent service communicating through a message bus. This gave us three critical properties:
- Isolation: A failure in the risk calculator did not affect the compression engine or the reporting pipeline.
- Independent scaling: We could scale the compression engine during peak trading hours without touching other services.
- Auditability: Every event was logged with a timestamp, producer, and consumer - providing a complete audit trail for regulators.
Diagram 2: Event-driven microservices architecture
The Message Bus
Kafka was our backbone. Every service produced events to topics and consumed from topics. This decoupling meant we could add new services (like a new regulatory report type) without changing existing ones:
// Simplified Kafka event schema
{
"event_id": "evt_20250315_001",
"event_type": "trade.settled",
"producer": "settlement-service",
"timestamp": "2025-03-15T14:30:00Z",
"payload": {
"trade_id": "IRS-2025-03-15-0042",
"counterparty": "BANK_A",
"notional_amount": 50000000.00,
"currency": "GBP",
"settlement_status": "confirmed"
}
}3. Trade Compression Engine
Trade compression is the algorithmic heart of the system. Two banks that have multiple offsetting interest rate swap positions can compress them - netting out the offsetting positions to reduce the total notional exposure. Fewer positions means less capital required under regulatory rules.
Diagram 3: Trade compression - before and after
Compression Algorithm
from dataclasses import dataclass
@dataclass
class InterestRateSwap:
id: str
counterparty: str
notional: float # Positive = receive, negative = pay
currency: str
maturity_date: str
fixed_rate: float
floating_rate_index: str
def find_compression_opportunities(
positions: list[InterestRateSwap],
) -> list[list[InterestRateSwap]]:
"""Group offsetting positions that can be compressed."""
# Group by counterparty + currency + maturity bucket
groups = {}
for pos in positions:
key = (pos.counterparty, pos.currency, pos.maturity_date[:7])
groups.setdefault(key, []).append(pos)
opportunities = []
for key, group in groups.items():
net_notional = sum(p.notional for p in group)
if abs(net_notional) < sum(p.notional for p in group) * 0.1:
# Offsetting positions found - net is < 10% of gross
opportunities.append(group)
return opportunities
# Example: 3 swaps with different directions
swaps = [
InterestRateSwap("A", "BANK_A", 50_000_000, "GBP", "2030-06", 0.035, "SONIA"),
InterestRateSwap("B", "BANK_A", -30_000_000, "GBP", "2030-06", 0.038, "SONIA"),
InterestRateSwap("C", "BANK_A", -20_000_000, "GBP", "2030-06", 0.036, "SONIA"),
]
opportunities = find_compression_opportunities(swaps)
print(f"Found {len(opportunities)} compression opportunities")
# Output: Found 1 compression opportunity ($50M + -$30M + -$20M = $0 net)4. Performance Optimization
The original compression engine ran a batch process every night that took 45 minutes. After optimization, we got it down to 11 minutes. Three changes drove that improvement:
1. Parallel Processing
The compression algorithm is embarassingly parallel - each counterparty group can be processed independently:
from concurrent.futures import ProcessPoolExecutor
import multiprocessing as mp
def parallel_compress(all_positions: list) -> list:
"""Process counterparty groups in parallel."""
groups = group_by_counterparty(all_positions)
with ProcessPoolExecutor(max_workers=mp.cpu_count()) as pool:
results = list(pool.map(compress_group, groups))
return merge_results(results)2. Intelligent Batching of Database Queries
The original system made one database query per trade. We replaced N+1 queries with batched bulk operations:
# Before: N+1 queries (one per trade)
for trade in trades:
counterparty = db.query("SELECT * FROM counterparties WHERE id = ?", trade.cp_id)
print(f"Loaded {trade.id}: {counterparty.name}")
# After: single batch query
ids = [trade.cp_id for trade in trades]
placeholders = ",".join("?" for _ in ids)
counterparties = db.query(
f"SELECT * FROM counterparties WHERE id IN ({placeholders})",
*ids,
)
cp_map = {c.id: c for c in counterparties}
# 50,000 individual queries → 1 batch query: 95% reductionDiagram 4: Performance optimization results
3. Caching with Redis
Frequently accessed reference data (counterparty details, instrument definitions) was cached in Redis:
import redis
import json
cache = redis.Redis(host="localhost", port=6379, decode_responses=True)
def get_counterparty(cp_id: str) -> dict:
"""Get counterparty data with Redis caching."""
cached = cache.get(f"cp:{cp_id}")
if cached:
return json.loads(cached)
counterparty = db.query_one(
"SELECT * FROM counterparties WHERE id = ?", cp_id
)
# Cache for 5 minutes (trades reference the same counterparties repeatedly)
cache.setex(f"cp:{cp_id}", 300, json.dumps(counterparty))
return counterparty
# Hit rate: ~85% during peak trading hours
# DB load reduced by 85% on read-heavy paths5. Regulatory Compliance Automation
Financial regulation is not optional. EMIR (European Market Infrastructure Regulation) and MiFID II require detailed reporting of every trade, including:
- Trade timestamps to the millisecond
- Counterparty identification with LEI codes
- Valuation details and collateral data
- Modification and cancellation history
We automated compliance by building checks into the event pipeline. Every event carried a compliance metadata envelope:
@dataclass
class ComplianceEnvelope:
event_id: str
event_type: str
timestamp_ns: int # Nanosecond precision
producer_service: str
producer_version: str
lei_codes: list[str] # Legal Entity Identifiers
audit_hash: str # SHA-256 of the payload for tamper proofing
def validate_compliance(event: ComplianceEnvelope) -> bool:
"""Check that an event meets regulatory requirements."""
checks = [
bool(event.lei_codes),
event.timestamp_ns > 0,
len(event.audit_hash) == 64,
event.producer_service in ALLOWED_SERVICES,
]
return all(checks)6. Lessons Learned
Building fintech systems at this scale taught me several lessons that apply well beyond trading:
Diagram 5: Key architectural principles for fintech
7. Summary: What to Remember
- Event-driven microservices provide the isolation and independent scaling that trading systems require. A failure in one service should never cascade.
- Trade compression is an algorithmic problem first - the parallel processing and caching come after you have the right algorithm.
- Intelligent batching (N+1 → bulk queries) produced the single biggest performance gain: 95% fewer database queries.
- Compliance must be built in, not bolted on. Every event carries its regulatory metadata from the moment it is created.
- Performance results: 75% faster processing, 62% less memory, 95% fewer queries - achieved through parallel processing, batching, and caching, not through magic.