Files
git.stella-ops.org/docs/workflow/engine/06-implementation-structure.md
master f5b5f24d95 Add StellaOps.Workflow engine: 14 libraries, WebService, 8 test projects
Extract product-agnostic workflow engine from Ablera.Serdica.Workflow into
standalone StellaOps.Workflow.* libraries targeting net10.0.

Libraries (14):
- Contracts, Abstractions (compiler, decompiler, expression runtime)
- Engine (execution, signaling, scheduling, projections, hosted services)
- ElkSharp (generic graph layout algorithm)
- Renderer.ElkSharp, Renderer.ElkJs, Renderer.Msagl, Renderer.Svg
- Signaling.Redis, Signaling.OracleAq
- DataStore.MongoDB, DataStore.PostgreSQL, DataStore.Oracle

WebService: ASP.NET Core Minimal API with 22 endpoints

Tests (8 projects, 109 tests pass):
- Engine.Tests (105 pass), WebService.Tests (4 E2E pass)
- Renderer.Tests, DataStore.MongoDB/Oracle/PostgreSQL.Tests
- Signaling.Redis.Tests, IntegrationTests.Shared

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 19:14:44 +02:00

7.3 KiB

06. Implementation Structure

1. Implementation Goal

The implementation should mirror the architecture instead of collapsing everything into Services/.

The code layout should make it obvious which parts are:

  • product orchestration
  • engine runtime
  • persistence
  • signaling
  • scheduling
  • operations

2. Proposed Folder Layout

Recommended new structure across the workflow host, shared abstractions, and external service contracts.

2.1 Service Host Project

Proposed folders:

Engine/
  Contracts/
  Execution/
  Persistence/
  Signaling/
  Scheduling/
  Hosting/
  Diagnostics/

Detailed proposal:

Engine/
  Contracts/
    IWorkflowRuntimeProvider.cs
    IWorkflowSignalBus.cs
    IWorkflowScheduleBus.cs
    IWorkflowRuntimeSnapshotStore.cs
    IWorkflowRuntimeDefinitionStore.cs

  Execution/
    SerdicaEngineRuntimeProvider.cs
    WorkflowExecutionCoordinator.cs
    WorkflowCanonicalInterpreter.cs
    WorkflowResumePointerSerializer.cs
    WorkflowExecutionSliceResult.cs
    WorkflowWaitDescriptor.cs
    WorkflowSubWorkflowCoordinator.cs
    WorkflowTransportDispatcher.cs

  Persistence/
    OracleWorkflowRuntimeSnapshotStore.cs
    WorkflowRuntimeSnapshotMapper.cs
    WorkflowRuntimeStateMutator.cs

  Signaling/
    OracleAqWorkflowSignalBus.cs
    WorkflowSignalEnvelope.cs
    WorkflowSignalPump.cs
    WorkflowSignalHandler.cs

  Scheduling/
    OracleAqWorkflowScheduleBus.cs
    WorkflowScheduleRequest.cs

  Hosting/
    WorkflowEngineSignalHostedService.cs
    WorkflowEngineStartupValidator.cs

  Diagnostics/
    WorkflowEngineMetrics.cs
    WorkflowEngineLogScope.cs

2.2 Shared Abstractions Project

Keep these in abstractions:

  • execution contracts
  • signal/schedule bus interfaces
  • runtime provider interfaces
  • runtime snapshot records where shared

Do not put Oracle-specific details into the shared abstractions project.

2.3 Contracts Project

Keep only external service contracts there.

Do not leak engine-internal snapshot or AQ message contracts into public workflow contracts.

3.1 Runtime Provider

public interface IWorkflowRuntimeProvider
{
    string ProviderName { get; }

    Task<WorkflowRuntimeExecutionResult> StartAsync(
        WorkflowRegistration registration,
        WorkflowDefinitionDescriptor definition,
        WorkflowBusinessReference? businessReference,
        StartWorkflowRequest request,
        object startRequest,
        CancellationToken cancellationToken = default);

    Task<WorkflowRuntimeExecutionResult> CompleteAsync(
        WorkflowRegistration registration,
        WorkflowDefinitionDescriptor definition,
        WorkflowTaskExecutionContext context,
        CancellationToken cancellationToken = default);
}

3.2 Snapshot Store

public interface IWorkflowRuntimeSnapshotStore
{
    Task<WorkflowRuntimeSnapshot?> GetAsync(
        string workflowInstanceId,
        CancellationToken cancellationToken = default);

    Task<bool> TryUpsertAsync(
        WorkflowRuntimeSnapshot snapshot,
        long expectedVersion,
        CancellationToken cancellationToken = default);
}

3.3 Signal Bus

public interface IWorkflowSignalBus
{
    Task PublishAsync(
        WorkflowSignalEnvelope envelope,
        CancellationToken cancellationToken = default);

    Task<IWorkflowSignalLease?> ReceiveAsync(
        string consumerName,
        CancellationToken cancellationToken = default);
}

3.4 Schedule Bus

public interface IWorkflowScheduleBus
{
    Task ScheduleAsync(
        WorkflowSignalEnvelope envelope,
        DateTime dueAtUtc,
        CancellationToken cancellationToken = default);
}

3.5 Definition Store

public interface IWorkflowRuntimeDefinitionStore
{
    WorkflowRuntimeDefinition GetRequiredDefinition(
        string workflowName,
        string workflowVersion);
}

4. Runtime Definition Normalization

Recommended startup path:

  1. read registrations from WorkflowRegistrationCatalog
  2. compile each workflow to canonical definition
  3. validate canonical definition
  4. convert to WorkflowRuntimeDefinition
  5. store in immutable in-memory cache

This startup step should be implemented once and reused by:

  • runtime execution
  • canonical inspection endpoints
  • diagnostics

5. Snapshot Model

Recommended runtime snapshot record:

public sealed record WorkflowRuntimeSnapshot
{
    public required string WorkflowInstanceId { get; init; }
    public required string WorkflowName { get; init; }
    public required string WorkflowVersion { get; init; }
    public required string RuntimeProvider { get; init; }
    public required long Version { get; init; }
    public WorkflowBusinessReference? BusinessReference { get; init; }
    public required string RuntimeStatus { get; init; }
    public required WorkflowEngineState EngineState { get; init; }
    public DateTime CreatedOnUtc { get; init; }
    public DateTime? CompletedOnUtc { get; init; }
    public DateTime LastUpdatedOnUtc { get; init; }
}

6. AQ Adapter Design

AQ adapters should be isolated behind backend-neutral interfaces.

Do not let the rest of the engine know about:

  • queue table names
  • enqueue option types
  • dequeue option types
  • AQ-specific exception types

That isolation is the main swap seam for any future non-AQ backend.

7. Transaction Boundary Design

7.1 Coordinator Owns Transactions

WorkflowExecutionCoordinator should own the unit of work for:

  • snapshot update
  • projection update
  • AQ publish
  • AQ dequeue completion

This avoids split responsibility across product services and engine helpers.

7.2 Projection Store Remains Focused

WorkflowProjectionStore should stay focused on:

  • read projection writes
  • query paths
  • task event history

It should not become the coordinator for AQ or engine versioning.

8. Startup Composition

WorkflowServiceCollectionExtensions should eventually compose the engine roughly like this:

services.Configure<WorkflowRuntimeOptions>(...);
services.Configure<WorkflowEngineOptions>(...);
services.Configure<WorkflowAqOptions>(...);

services.AddScoped<IWorkflowRuntimeProvider, SerdicaEngineRuntimeProvider>();
services.AddScoped<IWorkflowRuntimeOrchestrator, WorkflowRuntimeOrchestrator>();
services.AddScoped<IWorkflowRuntimeSnapshotStore, OracleWorkflowRuntimeSnapshotStore>();
services.AddScoped<IWorkflowSignalBus, OracleAqWorkflowSignalBus>();
services.AddScoped<IWorkflowScheduleBus, OracleAqWorkflowScheduleBus>();
services.AddSingleton<IWorkflowRuntimeDefinitionStore, WorkflowRuntimeDefinitionStore>();
services.AddHostedService<WorkflowEngineSignalHostedService>();

9. Avoided Anti-Patterns

The implementation should explicitly avoid:

  • a giant engine service that knows everything
  • polling tables for due work
  • in-memory only timer ownership
  • transport-specific engine branches scattered across the codebase
  • storing huge snapshots in AQ messages
  • mixing public contracts with engine internal contracts

10. Implementation Rules

  1. Put backend-specific code behind an interface.
  2. Keep canonical interpretation pure and backend-agnostic.
  3. Keep Oracle transaction handling close to the execution coordinator.
  4. Make resume idempotency part of the snapshot model, not a side utility.
  5. Keep projection writes product-oriented, not runtime-oriented.