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>
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. Recommended Core Interfaces
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:
- read registrations from
WorkflowRegistrationCatalog - compile each workflow to canonical definition
- validate canonical definition
- convert to
WorkflowRuntimeDefinition - 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
- Put backend-specific code behind an interface.
- Keep canonical interpretation pure and backend-agnostic.
- Keep Oracle transaction handling close to the execution coordinator.
- Make resume idempotency part of the snapshot model, not a side utility.
- Keep projection writes product-oriented, not runtime-oriented.