Architecture
The library is split into 4 projects with a strict dependency direction: Base → Core, Data → Base → Core, Util → Core + Base. Nothing in Core references Base. This is enforced by directory-level structure and is what makes the trait/concrete split possible.
Project map
flowchart TB
subgraph "Virtufin.Core (traits, no domain)"
Core_EAV[EAV abstractions<br/>IEntity, IAttribute,<br/>IValueProvider, Registry]
Core_Traits[Financial contract traits<br/>ITradeAction, ITradeEvent,<br/>IExecutionState]
Core_Decimal[DecimalAmount<br/>arbitrary-precision decimal]
Core_Position[Position trait<br/>IPosition, IQuantity]
end
subgraph "Virtufin.Base (concrete domain)"
Base_Currency[Currency, Cash, Stock<br/>PositionSize, Transaction]
Base_Scenario[Scenario, ScenarioHistory]
Base_Contract[ContractIdentifier, UTI,<br/>Signatory, AssetIdentifier]
Base_Exec[Executors<br/>Deterministic, Slippage,<br/>Live, ObserveOnly]
end
subgraph "Virtufin.Data (serialization + reactive)"
Data_Variant[Variant discriminated union]
Data_FB[FlatBuffers<br/>schema + codegen]
Data_NATS[NATS observable]
Data_WS[WebSocket observable]
Data_VK[Valkey observable]
end
subgraph "Virtufin.Util"
Util_Time[Time, TimeStamp]
Util_Fmt[Formatters]
end
Base_Currency --> Core_Decimal
Base_Scenario --> Core_EAV
Base_Contract --> Core_Traits
Base_Exec --> Core_Traits
Data_Variant --> Core_Decimal
Data_FB --> Data_Variant
Data_NATS --> Core_EAV
Util_Time --> Core_EAV
The trait/concrete split
The Core project defines traits (interfaces and types that don't depend on domain primitives). The Base project provides concrete implementations of those traits.
Core (trait) |
Base (concrete) |
|---|---|
ITradeAction (marker) |
RichTradeAction ADT (BuyOrder, SellOrder, CancelOrder, Transfer, Interest, WithdrawInterest) |
ITradeEvent<A> (with A Action property) |
TradeEvent ADT (OrderPlaced, OrderCancelled, OrderExpired, FillReceived, ExecutionCleared, ...) |
IExecutionState (marker) |
DeterministicState (empty), StochasticState (seeded PRNG) |
Position (record) |
PositionSize (concrete), PositionTransferContract (concrete) |
IQuantity (trait) |
DecimalAmountQuantity, SignedDecimalAmountQuantity |
Why split this way? Two reasons:
- Substitutability: a function declared as
IExecutor<TState, ITradeAction, ITradeEvent<ITradeAction>>is callable with anyIExecutor<TState, RichTradeAction, TradeEvent>(becauseRichTradeAction : ITradeActionandTradeEvent : ITradeEvent<RichTradeAction>, andITradeEvent<out A>is covariant). - Testability: tests can supply a minimal
IQuantitystub without depending onDecimalAmount-backed types.
The convention is enforced by direction: Base may import Core, but Core never imports Base. Code review at the PR level catches violations.
The Variant cross-cutting type
Variant lives in Virtufin.Data but uses DecimalAmount from Core and IQuantity from Core for the numeric cases. It is the lingua franca for the platform: code in WorkManager, WebSocketManager, API Gateway, and external workers all produce and consume Variant values over the wire.
The FlatBuffers schema in Virtufin.Data lets a Variant produced in C# be deserialized in Python and TypeScript with the same shape — see Variant Type.
Reactive layer
Virtufin.Data provides three IObservable<T> adapters, all with auto-reconnect:
| Adapter | Source | Notes |
|---|---|---|
NatsObservable |
NATS subjects | Subject-pattern match, re-subscribes on reconnect |
WebSocketObservable |
WebSocket frames | Reconnects with exponential backoff |
ValkeyObservable |
Valkey keyspace notifications | Key-pattern match, re-subscribes on PSUB reconnect |
All three are documented under Observables.
The execution algebra
IExecutor<TState, TAction, TEvent> is the algebra of "given a current state and an action, produce the next state and an outcome event." The library provides four implementations:
| Class | Algorithm shape | When to use |
|---|---|---|
DeterministicExecutorBase<TState, TAction, TEvent> |
Execute → Process(state, action) |
Backtests, unit tests, deterministic simulations |
SlippageExecutorBase<TState, TAction, TEvent> |
Execute → CalculateSlippage(state, action) → Process(state, action, slippage) |
Realistic paper trading with slippage modeling |
LiveExecutorBase<TState, TAction, TEvent> |
Execute → Process(state, action) |
Production exchange routing (subclass) |
ObserveOnlyExecutor<TState, TAction, TEvent> |
Decorator — wraps another IExecutor and records events |
SHADOW_* scenarios |
All three base classes are abstract — subclasses define the case-by-case fill logic. The bases differ in algorithm shape, not in name or config field. See DecimalAmount for the numeric type used in fills.
NativeAOT compatibility
The library is built to be NativeAOT-compatible:
- All public APIs are AOT-friendly (no reflection on user types, no late binding)
- JSON serialization uses source-generated JsonSerializerContexts where applicable
- FlatBuffers serialization is codegen, not reflection
If you're consuming this library from an AOT-published project, the Variant and DecimalAmount types work without any AOT warnings. Subclassing IExecutor from an AOT project requires care with the case-by-case pattern matching — DynamicallyAccessedMembers annotations may be needed on the TAction type parameter.
Project file conventions
Each project follows these conventions:
- <TargetFramework>net10.0</TargetFramework>
- <Nullable>enable</Nullable>
- <ImplicitUsings>enable</ImplicitUsings>
- <LangVersion>preview</LangVersion> (C# 14 preview features)
- <TreatWarningsAsErrors>true</TreatWarningsAsErrors> (CI enforces clean builds)
- Line width 80 characters, Allman braces, 4-space indent
- File-scoped namespaces preferred
Package versions are managed centrally in Directory.Packages.props. No <Version> elements in .csproj files.