Skip to content

Architecture

Detailed architecture documentation for the Virtufin API gateway.

System Overview

Virtufin API is a gRPC-based API gateway that provides:

  1. Service Discovery - Dynamic discovery via gRPC reflection
  2. Method Invocation - Transparent forwarding to backend services
  3. Event Streaming - gRPC streaming and Dapr pub/sub
  4. State Management - Dapr state store integration
  5. Multi-Protocol Support - REST, gRPC, and gRPC-Web

System Architecture

flowchart TB
    subgraph Clients["Clients"]
        CSharp[C# Client]
        Python[Python Client]
        Browser[Browser<br/>gRPC-Web]
        REST[REST Client<br/>Swagger]
    end

    subgraph ProtocolLayer[Protocol Layer]
        Shared[Dual Ports<br/>HTTP:5001 / gRPC:5002]
    end

    subgraph ApplicationLayer["Application Layer"]
        Kestrel[Kestrel Server]
        GatewaySvc[GatewayService]
        PubsubSvc[PubsubService]
        StateSvc[StateService]
    end

    subgraph Infrastructure["Infrastructure"]
        DaprClient[Dapr Client]
        ChannelPool[gRPC Channel Pool]
    end

    subgraph Backend["Backend Services"]
        WM[WorkManager<br/>:5002]
        WSM[WebSocketManager<br/>:5002]
    end

    subgraph Dapr["Dapr Sidecar"]
        State[Dapr State]
        PubSub[Dapr PubSub]
    end

    Clients --> ProtocolLayer
    ProtocolLayer --> ApplicationLayer
    ApplicationLayer --> Infrastructure
    Infrastructure --> Backend
    Infrastructure --> Dapr

Component Architecture

flowchart LR
    subgraph Clients
        C[C#]
        P[Python]
        B[Browser]
    end

    subgraph Gateway["Virtufin.Api"]
        G[gRPC Gateway]
        E[Event System]
        S[State Service]
    end

    subgraph Backends
        WM[WorkManager]
        WSM[WebSocketManager]
    end

    C --> G
    P --> G
    B --> G
    G --> WM
    G --> WSM
    G <--> S
    S <--> State
    E <--> PubSub

Component Details

Component Architecture

flowchart TB
    subgraph Clients[Clients]
        CSharp[C# Client]
        Python[Python Client]
        Browser[Browser gRPC-Web]
        REST[REST HTTP Swagger]
    end

    subgraph ProtocolLayer[Protocol Layer]
        Shared[Dual Ports<br/>HTTP:5001 / gRPC:5002]
    end

    subgraph ApplicationLayer[Application Layer]
        Kestrel[Kestrel Server]
        RESTCtrl[REST Controllers]
        GatewaySvc[GatewayService gRPC]
        ConfigSvc[ConfigService gRPC]
        GrpcReflSvc[GrpcReflectionService gRPC]
        PubsubSvc[PubsubService]
        StateSvc[StateService]
    end

    subgraph Infrastructure[Infrastructure]
        ChannelPool[GrpcChannelPool]
        DaprClient[DaprClient]
        ConfigLoader[ServicesConfigurationLoader]
    end

    subgraph Backend[Backend Services]
        WM[WorkManager :5002]
        WSM[WebSocketManager :5002]
    end

    subgraph Dapr[Dapr Sidecar]
        State[Dapr State]
        PubSub[Dapr PubSub]
    end

    Clients --> ProtocolLayer
    ProtocolLayer --> ApplicationLayer
    ApplicationLayer --> Infrastructure
    Infrastructure --> Backend
    Infrastructure --> Dapr

System Architecture

flowchart TB
    subgraph Clients[Clients]
        CSharp[C# Client]
        Python[Python Client]
        Browser[Browser gRPC-Web]
        REST[REST HTTP Swagger]
    end

    subgraph ProtocolLayer[Protocol Layer]
        Shared[Dual Ports<br/>HTTP:5001 / gRPC:5002]
    end

    subgraph ApplicationLayer[Application Layer]
        Kestrel[Kestrel Server]
        RESTCtrl[REST Controllers]
        GatewaySvc[GatewayService gRPC]
        ConfigSvc[ConfigService gRPC]
        GrpcReflSvc[GrpcReflectionService gRPC]
        PubsubSvc[PubsubService]
        StateSvc[StateService]
    end

    subgraph Infrastructure[Infrastructure]
        ChannelPool[GrpcChannelPool]
        DaprClient[DaprClient]
        ConfigLoader[ServicesConfigurationLoader]
    end

    subgraph Backend[Backend Services]
        WM[WorkManager :5002]
        WSM[WebSocketManager :5002]
    end

    subgraph Dapr[Dapr Sidecar]
        State[Dapr State]
        PubSub[Dapr PubSub]
    end

    Clients --> ProtocolLayer
    ProtocolLayer --> ApplicationLayer
    ApplicationLayer --> Infrastructure
    Infrastructure --> Backend
    Infrastructure --> Dapr

Method Invocation (REST)

sequenceDiagram
    participant Client as curl
    participant Kestrel as Kestrel
    participant Gateway as GrpcGateway Extensions
    participant Pool as GrpcChannel Pool
    participant Backend as Backend Service :5002
    participant Reflection as gRPC Reflection

    Client->>Kestrel: HTTP/REST JSON
    Kestrel->>Gateway: JSON
    Gateway->>Pool: GetChannel(service)
    Pool->>Backend: gRPC Call
    Backend->>Reflection: Reflection Request
    Reflection-->>Backend: Service Descriptors
    Backend-->>Pool: Response
    Pool-->>Gateway: InvokeResponse
    Gateway-->>Kestrel: JSON
    Kestrel-->>Client: HTTP Response

gRPC Invocation

sequenceDiagram
    participant Client as C# Client
    participant Kestrel as Kestrel
    participant Gateway as Gateway Service
    participant Pool as GrpcChannel Pool
    participant Backend as Backend Service :5002

    Client->>Kestrel: gRPC
    Kestrel->>Gateway: InvokeRequest
    Gateway->>Pool: GetChannel(service)
    Pool->>Backend: gRPC Call
    Backend-->>Pool: Response
    Pool-->>Gateway: InvokeResponse
    Gateway-->>Kestrel: InvokeResponse
    Kestrel-->>Client: gRPC Response

Event Streaming (Dapr → gRPC)

flowchart LR
    Dapr[Dapr Sidecar] --> PubSub[PubSub Event]
    PubSub --> Registry[TopicDaprSubscriptionRegistry]
    DaprStreamMgr --> SubMgr[PubsubSubscriptionManager]
    PubsubSvc --> GrpcClient[gRPC Client Subscribe]

Configuration Architecture

flowchart TB
    ConfigFile[services.json] --> Loader[ServicesConfigurationLoader]
    Loader --> ServicesConfig[ServicesConfiguration]
    ServicesConfig --> ChannelPool[GrpcChannelPool]
    ServicesConfig --> Reflection[GrpcReflectionService]
    ServicesConfig --> PubsubSvc[PubSubService]
    ServicesConfig --> State[StateService]
    ServicesConfig --> Config[ConfigService]

GrpcChannelPool

Location: Services/GrpcChannelPool.cs

The GrpcChannelPool maintains a thread-safe pool of gRPC channels, one per backend service.

Key Features: - Lazy channel creation (created on first access) - Channel reuse across invocations - LRU eviction when channel limit is reached - Automatic cleanup on application shutdown - Configuration-driven service endpoints

Flow:

sequenceDiagram
    participant Client
    participant Gateway as GatewayService
    participant Pool as GrpcChannelPool
    participant Cache as Channel Cache
    participant Backend as Backend Service

    Client->>Gateway: Invoke(service, method, data)
    Gateway->>Pool: GetChannel(serviceName)
    Pool->>Cache: Check cached?
    Cache-->>Pool: Not found
    Pool->>Pool: Create new GrpcChannel
    Pool->>Cache: Store in cache
    Cache-->>Pool: Channel
    Pool-->>Gateway: Channel
    Gateway->>Backend: gRPC Call
    Backend-->>Gateway: Response
    Gateway-->>Client: InvokeResponse

Configuration (services.json):

services:
  - name: workmanager
    host: localhost
    port: 5002
    protocol: grpc


GrpcReflectionService

Location: Services/GrpcReflectionService.cs

Provides dynamic service discovery and method invocation using gRPC reflection.

Key Features: - Fetches service descriptors from backend services via reflection - Caches descriptors per service for performance - Uses TypeRegistry for robust JSON↔Protobuf conversion - Handles well-known type descriptors

Class Structure:

Class Purpose
GrpcReflectionService Public API, manages cache instances
ServiceDescriptorCache Per-service cache, loads and parses descriptors
ServiceMethodInfo Method metadata (name, input/output types, streaming)
MethodSchema Schema for request/response types
FieldSchema Individual field definitions
GrpcCallResult Invocation result wrapper

Discovery Flow:

sequenceDiagram
    participant Client as GatewayClient
    participant Cache as ServiceDescriptorCache
    participant Reflection as gRPC Reflection
    participant Backend as Backend Service

    Client->>Cache: GetMethodsAsync(serviceName)
    Cache->>Cache: GetOrCreateCache()
    Cache->>Reflection: ServerReflectionInfo()
    Reflection->>Backend: ServerReflectionInfo()
    Backend-->>Reflection: FileDescriptorProtos
    Reflection-->>Cache: Descriptors
    Cache->>Cache: Parse and Index
    Cache-->>Client: List<MethodInfo>

Invocation Flow:

sequenceDiagram
    participant Client
    participant Gateway as GatewayService
    participant Cache as ServiceDescriptorCache
    participant Channel as gRPC Channel
    participant Backend as Backend Service

    Client->>Gateway: Invoke(request)
    Gateway->>Cache: GetOrCreateCache(service)
    Cache-->>Gateway: Cached descriptors
    Gateway->>Gateway: Create protobuf from JSON
    Gateway->>Channel: AsyncUnaryCall()
    Channel->>Backend: gRPC Request
    Backend-->>Channel: gRPC Response
    Channel-->>Gateway: Response bytes
    Gateway->>Gateway: Parse to JSON
    Gateway-->>Client: GrpcCallResult

Invocation Flow:

flowchart TD
    A[ExecuteCallAsync<br/>service, method, json] --> B[GetOrCreateCache<br/>EnsureLoadedAsync]
    B --> C[Find method by name]
    C --> D[Create protobuf message<br/>from JSON]
    D --> E[Create generic<br/>Method byte array byte array]
    E --> F[channel.CreateCallInvoker<br/>AsyncUnaryCall]
    F --> G[Parse response bytes<br/>to JSON]
    G --> H[Return GrpcCallResult]

GatewayService

Location: Services/GatewayService.cs

The main gRPC service implementing the Gateway protocol.

gRPC Methods:

Method Type Description
Invoke Unary Invoke a method on a backend service
ListServices Unary List all registered services
ListMethods Unary List methods for a service
Subscribe Server Streaming Subscribe to events

Invoke Flow:

flowchart TD
    A[Client calls<br/>Gateway.Invoke<br/>InvokeRequest] --> B[Extract request headers<br/>as metadata]
    B --> C[Call ReflectionService<br/>ExecuteCallAsync]
    C --> D[Map result to<br/>InvokeResponse]
    D --> E[Return to client]

PubsubService

Location: Services/PubsubService.cs

Implements the Pubsub gRPC service: PublishEvent, Subscribe, Unsubscribe. Subscriptions are filtered by topic only — there is no scope hierarchy, no service filter, and no event-type filter. A subscriber that needs finer granularity inspects cloudevent.type itself.

Subscribe creates one bounded Channel (1024 events) per call, shared across all topics named in that call, and multiplexes every topic's events into the single response stream. A consumer that falls further behind than the bound is reaped by the health sweeper.

Response headers are flushed before any event is written. Request-reply clients depend on this to know the subscription is live before they publish — without it, a client waits for metadata that cannot arrive until it publishes, and won't publish until metadata arrives.

TopicDaprSubscriptionRegistry

Location: Services/TopicDaprSubscriptionRegistry.cs

Holds one shared Dapr streaming subscription per topic per process, reference-counted across all gRPC callers subscribed to that topic. A subscription per caller would make daprd load-balance each event across callers instead of broadcasting it, so callers share one and fan out in-process.

sequenceDiagram
    participant Client
    participant Pubsub as PubsubService
    participant Registry as TopicDaprSubscriptionRegistry
    participant SubMgr as PubsubSubscriptionManager
    participant Dapr as Dapr Sidecar

    Client->>Pubsub: Subscribe(topics)
    Pubsub->>SubMgr: Subscribe(topic, channelWriter)
    Pubsub->>Registry: AcquireAsync(topic, onMessage)
    alt first caller for this topic
        Registry->>Dapr: SubscribeAsync(pubsub, topic, handler)
    else topic already subscribed
        Registry->>Registry: AddRef()
    end
    Pubsub->>Client: flush response headers

    Dapr->>Registry: TopicMessage
    Registry->>SubMgr: BroadcastToTopicAsync(topic, cloudevent)
    SubMgr->>Client: SubscribeResponse

    Client->>Pubsub: cancel call
    Pubsub->>SubMgr: UnsubscribeById
    Pubsub->>Registry: DisposeAsync (RemoveRef)
    Registry->>Dapr: dispose subscription when refcount hits zero

PubsubSubscriptionManager

Location: Services/PubsubSubscriptionManager.cs (base: Services/SubscriptionManagerBase.cs)

In-process fan-out: maps "pubsub/{topic}" to the set of gRPC callers streaming that topic, each identified by a GUID subscription id. Writes to a subscriber are bounded by a 5-second timeout; a failed write is logged and dropped rather than failing the publish.

DaprAppCallbackService

Location: Services/DaprAppCallbackService.cs

Receives Dapr Scheduler job callbacks and publishes an empty CloudEvent to the job's target_topic. Bound on both Dapr's alpha and stable AppCallback routes — DaprStableAppCallbackMethodProvider adds the stable one, which the .NET SDK does not generate. Because subscriptions are created programmatically rather than declared, the ListTopicSubscriptions callback deliberately returns an empty list.

Data Flow Diagrams

Method Invocation (REST)

sequenceDiagram
    participant Client as curl
    participant Kestrel as Kestrel
    participant Gateway as GrpcGateway Extensions
    participant Pool as GrpcChannel Pool
    participant Backend as Backend Service :5002
    participant Reflection as gRPC Reflection

    Client->>Kestrel: HTTP/REST JSON
    Kestrel->>Gateway: JSON
    Gateway->>Pool: GetChannel(service)
    Pool->>Backend: gRPC Call
    Backend->>Reflection: Reflection Request
    Reflection-->>Backend: Service Descriptors
    Backend-->>Pool: Response
    Pool-->>Gateway: InvokeResponse
    Gateway-->>Kestrel: JSON
    Kestrel-->>Client: HTTP Response

gRPC Invocation

sequenceDiagram
    participant Client as C# Client
    participant Kestrel as Kestrel
    participant Gateway as Gateway Service
    participant Pool as GrpcChannel Pool
    participant Backend as Backend Service :5002

    Client->>Kestrel: gRPC
    Kestrel->>Gateway: InvokeRequest
    Gateway->>Pool: GetChannel(service)
    Pool->>Backend: gRPC Call
    Backend-->>Pool: Response
    Pool-->>Gateway: InvokeResponse
    Gateway-->>Kestrel: InvokeResponse
    Kestrel-->>Client: gRPC Response

Event Streaming (Dapr → gRPC)

flowchart LR
    Dapr[Dapr Sidecar] -->|Pub/Sub| Registry[TopicDaprSubscriptionRegistry]
    Registry -->|event| SubMgr[PubsubSubscriptionManager]
    SubMgr --> GrpcClient[gRPC Client Subscribe]

Configuration Architecture

flowchart TB
    ConfigFile[services.json] --> Loader[ServicesConfigurationLoader]
    Loader --> ServicesConfig[ServicesConfiguration]
    ServicesConfig --> ChannelPool[GrpcChannelPool]
    ServicesConfig --> Gateway[GatewayService]
    ServicesConfig --> Reflection[AggregatedReflectionService]
    ServicesConfig --> State["StateService<br/>(store name only)"]
    ServicesConfig --> Config[ConfigService]

Health Checks

Check Name Healthy when
DaprHealthCheck dapr Dapr sidecar is reachable
GrpcChannelPoolHealthCheck grpc-channel-pool Channel pool has channels for the configured services
GatewayCircuitBreakerHealthCheck dapr-circuit-breaker The gateway circuit breaker is not open
ApiAuthHealthCheck api-auth API-key auth is configured as expected

All four run on /health and /healthz. A gRPC health service is also mapped at grpc.health.v1.Health, which the API-key interceptor exempts from authentication.