Skip to content

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.