Architecture
Detailed architecture documentation for the Virtufin API gateway.
System Overview
Virtufin API is a gRPC-based API gateway that provides:
- Service Discovery - Dynamic discovery via gRPC reflection
- Method Invocation - Transparent forwarding to backend services
- Event Streaming - gRPC streaming and Dapr pub/sub
- State Management - Dapr state store integration
- 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.