Architecture
The platform in three diagrams. Component-level detail lives in the Components page; protocol-level detail in the OpenSpec.
System context
flowchart TB
subgraph Clients
CSharp[C# Client]
Python[Python Client]
TS[TypeScript Client]
REST[REST / curl / Swagger]
end
subgraph Edge["Edge — single entry point"]
API[Virtufin API Gateway<br/>gRPC :5002 / REST :5001]
end
subgraph Services["Virtufin services"]
WSM[WebSocketManager<br/>gRPC :5002]
WM[WorkManager<br/>gRPC :25002]
end
subgraph Workers["Worker processes"]
DLL[.NET DLL workers<br/>in-process]
PY[Python workers<br/>subprocess]
NAT[Native workers<br/>in-process]
CS[C# source workers<br/>in-process Roslyn]
end
subgraph Sidecar["Dapr sidecar (per pod)"]
PS[(Pub/Sub<br/>NATS / Redis)]
ST[(State<br/>Redis / Cosmos)]
end
subgraph Data["Upstream data"]
EX[Exchanges / feeds<br/>Binance, IB, FX, etc.]
end
Clients --> API
API <--> PS
API <--> ST
API --> WSM
API --> WM
WSM --> EX
WM --> DLL
WM --> PY
WM --> NAT
WM --> CS
WM <--> PS
WM <--> ST
The two things to notice: the API gateway is the only public face, and Dapr is the only thing that talks to the broker / state store. Services never see NATS, never see Redis, never see each other's endpoints directly.
Request lifecycle
A typical strategy call: a market-data tick arrives, the WebSocketManager publishes it, the WorkManager routes it to a worker, the worker produces an order event, the API gateway records the result.
sequenceDiagram
participant EX as Exchange
participant WSM as WebSocketManager
participant DAPR as Dapr PubSub
participant API as API Gateway
participant WM as WorkManager
participant W as Worker (in-process)
participant ST as State (via API)
EX->>WSM: WS frame (orderbook update)
WSM->>WSM: Build CloudEvent v1.0 envelope<br/>(lane: act.exchange.binance.orderbook.update)
WSM->>DAPR: PublishEvent(topic, event)
DAPR-->>WSM: ack
Note over WM,W: WorkManager subscribes via API.Subscribe
DAPR->>API: stream event to subscribers
API->>WM: server-streaming gRPC
WM->>W: Dispatch to DotNetDllEngine
W->>W: compute signal
W->>WM: produce order event
WM->>API: PublishEvent(topic=sc.LIVE.orders, event)
API->>ST: SaveState(service="workmanager", order_id, ...)
ST-->>API: ok
API-->>WM: ack
Event envelope (CloudEvents 1.0)
Every event on the bus is the same shape. The Virtufin-specific extensions travel in the same envelope.
flowchart LR
subgraph CE["CloudEvents 1.0 envelope"]
ID["id"]
SRC["source<br/>(urn:com.virtufin.<service>)"]
TYPE["type<br/>(com.virtufin.<service>.<entity>.<event>)"]
SUBJ["subject<br/>(<entity_type>/<entity_id>)"]
TIME["time (eventtime)"]
DATA["data<br/>(payload)"]
end
subgraph VX["Virtufin extensions"]
CID["correlationid"]
SID["scenarioid"]
RID["runid"]
EXCH["exchange"]
CLK["clocktype (wall | historical)"]
ET["eventtime"]
WT["walltime"]
ML["market.lane"]
MS["market.selector"]
PL["portfolio.lane"]
SL["strategy.lane"]
end
CE --- VX
Routing decisions are made on (ce-type, subject, scenarioid,
clocktype). The same producer can target production and backtest
by switching the topic address from act.* to hyp.<selector>.*
or from sc.LIVE.* to sc.<backtest-id>.*; the envelope itself
is identical.
For the formal definition, see pubsub-topics spec.