Optima.Net.Domain
1.0.2
Prefix Reserved
See the version list below for details.
dotnet add package Optima.Net.Domain --version 1.0.2
NuGet\Install-Package Optima.Net.Domain -Version 1.0.2
<PackageReference Include="Optima.Net.Domain" Version="1.0.2" />
<PackageVersion Include="Optima.Net.Domain" Version="1.0.2" />
<PackageReference Include="Optima.Net.Domain" />
paket add Optima.Net.Domain --version 1.0.2
#r "nuget: Optima.Net.Domain, 1.0.2"
#:package Optima.Net.Domain@1.0.2
#addin nuget:?package=Optima.Net.Domain&version=1.0.2
#tool nuget:?package=Optima.Net.Domain&version=1.0.2
Optima.Net.Domain
Optima.Net.Domain is a lightweight, framework-agnostic toolkit that provides explicit building blocks for writing clear, maintainable, and auditable Domain-Driven Design (DDD) code.
This library is intentionally focused. It does not try to be a framework. Instead, it provides a small set of primitives that help you express facts, decisions, and outcomes in a way that scales from simple business rules to finance-grade decision systems.
This document is both:
- a comprehensive reference, and
- a step-by-step tutorial for engineers new to DDD-style modelling.
If you read it top-to-bottom, you should be able to confidently apply Specifications, Policies, and Anti-Corruption Layers in real systems.
If you like what I did here or it inspired you to learn something new, please consider buying me a coffee: https://buymeacoffee.com/snamretsuek The support would be greatly appreciated!
Table of Contents
- Motivation and Mental Model
- Core Concepts Overview
- Specifications (Sync))
- Composite Specifications
- Policies (Sync)
- Composite Policies
- Async Specifications and Policies
- Diagnostics}
- Diagnostic Helpers
- Decisions / Outcomes)
- Identity and Naming
- End-to-End Examples
- Anti-Corruption Layers (ACL)
- ACL – Real-World Code Examples
- ACL Anti-Patterns
- Architectural Enforcement & Tests
- Application Layer Primitives
- UseCase Example
- Saga Example
- Design Philosophy and Boundaries
1. Motivation and Mental Model
In many systems, business rules end up:
- buried in
ifstatements, - duplicated across services,
- tightly coupled to infrastructure,
- or impossible to explain after the fact.
Optima.Net.Domain exists to fix this by making rules and decisions explicit.
The Mental Model
| Concept | Purpose | Question Answered |
|---|---|---|
| Specification | Fact | Is this true? |
| Policy | Decision | May we proceed? |
| Diagnostics | Explanation | Why did this fail? |
| Decision | Outcome | What happened? |
This separation is the foundation of the library.
2. Core Concepts Overview
Specifications
- Express facts
- Are small and composable
- Contain no side effects
- Can be reused across policies
Policies
- Express business intent
- Are fulfilled only if all required facts hold
- Often composed from multiple specifications
- Can be composed from other policies
Diagnostics
- Observe evaluation
- Never change behaviour
- Produce immutable result trees
Decisions
- Capture evaluation outcomes
- Are immutable and serializable
- Suitable for logging and auditing
3. Specifications (Sync)
A specification answers a single factual question.
Example Domain: Orders
public sealed class OrderLine
{
public string Product { get; }
public decimal Price { get; }
public bool CanShipByAir { get; }
public OrderLine(string product, decimal price, bool canShipByAir)
{
Product = product;
Price = price;
CanShipByAir = canShipByAir;
}
}
public sealed class Order
{
private readonly List<OrderLine> _lines = new();
public IReadOnlyCollection<OrderLine> Lines => _lines;
public decimal Total => _lines.Sum(l => l.Price);
public void AddLine(OrderLine line) => _lines.Add(line);
}
Individual Specifications
public sealed class OrderHasLinesSpec : Specification<Order>
{
public override bool IsSatisfiedBy(Order order)
=> order.Lines.Any();
}
public sealed class OrderCanShipByAirSpec : Specification<Order>
{
public override bool IsSatisfiedBy(Order order)
=> order.Lines.All(l => l.CanShipByAir);
}
public sealed class OrderTotalExceedsSpec : Specification<Order>
{
private readonly decimal _threshold;
public OrderTotalExceedsSpec(decimal threshold)
=> _threshold = threshold;
public override bool IsSatisfiedBy(Order order)
=> order.Total > _threshold;
}
Each specification:
- Answers one question
- Has no knowledge of business intent
- Is independently testable
4. Composite Specifications
Specifications can be composed to express more complex facts.
var shippableOrderSpec =
new OrderHasLinesSpec()
.And(new OrderCanShipByAirSpec());
var discountEligibleSpec =
new OrderHasLinesSpec()
.And(new OrderTotalExceedsSpec(500m));
Composite specifications still express facts. They do not represent decisions.
5. Policies (Sync)
Policies express decisions and are typically built from specifications.
Order Fulfillment Policy
public sealed class OrderFulfillmentPolicy : Policy<Order>
{
private static readonly ISpecification<Order> _specification =
new OrderHasLinesSpec()
.And(new OrderCanShipByAirSpec());
public override bool IsSatisfiedBy(Order order)
=> _specification.IsSatisfiedBy(order);
}
This policy answers:
May this order be fulfilled?
Discount Eligibility Policy
public sealed class OrderDiscountPolicy : Policy<Order>
{
private static readonly ISpecification<Order> _specification =
new OrderHasLinesSpec()
.And(new OrderTotalExceedsSpec(500m));
public override bool IsSatisfiedBy(Order order)
=> _specification.IsSatisfiedBy(order);
}
6. Composite Policies
In many domains, multiple policies must be fulfilled before progression is allowed.
Finance Example: Credit Application
public sealed class CreditApplication
{
public decimal Amount { get; }
public bool HasPriorDefaults { get; }
public bool PassedAmlChecks { get; }
public CreditApplication(
decimal amount,
bool hasPriorDefaults,
bool passedAmlChecks)
{
Amount = amount;
HasPriorDefaults = hasPriorDefaults;
PassedAmlChecks = passedAmlChecks;
}
}
Specifications
public sealed class CreditWithinLimitSpec : Specification<CreditApplication>
{
public override bool IsSatisfiedBy(CreditApplication app)
=> app.Amount <= 100_000m;
}
public sealed class NoPriorDefaultsSpec : Specification<CreditApplication>
{
public override bool IsSatisfiedBy(CreditApplication app)
=> !app.HasPriorDefaults;
}
public sealed class PassedAmlChecksSpec : Specification<CreditApplication>
{
public override bool IsSatisfiedBy(CreditApplication app)
=> app.PassedAmlChecks;
}
Policies Built from Specifications
public sealed class CreditApprovalPolicy : Policy<CreditApplication>
{
private static readonly ISpecification<CreditApplication> _specification =
new CreditWithinLimitSpec()
.And(new NoPriorDefaultsSpec());
public override bool IsSatisfiedBy(CreditApplication app)
=> _specification.IsSatisfiedBy(app);
}
public sealed class AmlClearancePolicy : Policy<CreditApplication>
{
private static readonly ISpecification<CreditApplication> _specification =
new PassedAmlChecksSpec();
public override bool IsSatisfiedBy(CreditApplication app)
=> _specification.IsSatisfiedBy(app);
}
Composite Policy Gate
public sealed class CreditIssuancePolicy
: CompositePolicy<CreditApplication>
{
protected override IReadOnlyCollection<IPolicy<CreditApplication>> Policies =>
new IPolicy<CreditApplication>[]
{
new CreditApprovalPolicy(),
new AmlClearancePolicy()
};
}
This expresses a progression gate:
Credit may only be issued if all required policies are fulfilled.
7. Async Specifications and Policies
Async variants exist for:
- Database checks
- External services
- Fraud / AML systems
public sealed class AsyncAmlSpec : AsyncSpecification<CreditApplication>
{
public override Task<bool> IsSatisfiedByAsync(
CreditApplication app,
CancellationToken ct = default)
=> Task.FromResult(app.PassedAmlChecks);
}
Async policies mirror sync policies exactly. Sync and async are never mixed.
8. Diagnostics
Diagnostics explain why something passed or failed.
Specification Diagnostics
var evaluator = new DiagnosticSpecificationEvaluator();
var diagnostics = evaluator.Evaluate(specification, order);
Policy Diagnostics
var evaluator = new PolicyDiagnosticEvaluator();
var diagnostics = evaluator.Evaluate(policy, application);
Diagnostics produce immutable trees describing evaluation.
9. Diagnostic Helpers
Helpers make diagnostics usable without changing behaviour.
var failedSpecs = diagnostics.FailedSpecificationTypes();
var failedPolicies = diagnostics.FailedPolicyTypes();
var leafFailures = diagnostics.FailedLeaves();
10. Decisions / Outcomes
Decisions capture evaluation outcomes.
var decision = policy.Decide(application);
public sealed record PolicyDecision(
string PolicyType,
bool Fulfilled);
Decisions are:
- Immutable
- Serializable
- Suitable for audit trails
11. Identity and Naming
Diagnostics use structural identity by default:
PolicyType = policy.GetType().Name
Optional interfaces allow human-readable naming without affecting diagnostics.
12. End-to-End Example
if (!creditIssuancePolicy.IsSatisfiedBy(application))
{
var diagnostics =
policyDiagnosticEvaluator.Evaluate(
creditIssuancePolicy,
application);
var failedPolicies = diagnostics.FailedPolicyTypes();
}
This flow is:
- Explicit
- Deterministic
- Auditable
- Framework-free
13. Anti-Corruption Layers (ACL)
Purpose
Anti-Corruption Layers exist to protect the domain from foreign models, foreign semantics, and foreign change.
An ACL is not a mapper. It is not infrastructure. It is not a shared contract.
It is a semantic firewall.
If an external system changes tomorrow, the domain must not notice.
Intent
An ACL:
- Translates foreign meaning into domain meaning
- Enforces domain invariants before the domain is touched
- Rejects unsupported or ambiguous concepts explicitly
- Absorbs versioning and contract drift
- Prevents domain pollution
Explicit Non-Goals
ACLs do not:
- Call HTTP APIs
- Deserialize JSON
- Publish messages
- Retry operations
- Log infrastructure errors
- Share DTOs or enums
Those concerns belong elsewhere.
Inbound vs Outbound ACLs
Inbound ACL (Domain Protection)
- Direction: External → Domain
- Strict and defensive
- Explicitly rejects invalid input
- Returns semantic translation outcomes
public interface IInboundTranslator<in TForeign, TDomain>
{
TranslationResult<TDomain> Translate(TForeign input);
}
Outbound ACL (Consumer Protection)
- Direction: Domain → External
- Shapes domain data for consumers
- Failure is exceptional
public interface IOutboundTranslator<TDomain, TForeign>
{
TForeign Translate(TDomain input);
}
Inbound and outbound ACLs solve different problems and must never be merged.
14. ACL – Real-World Code Examples
This section demonstrates production-grade ACL usage. These examples are intentionally non-trivial.
Example Scenario
You integrate with a third-party Payments API that:
- Has breaking versions
- Uses terminology incompatible with your domain
- Allows invalid state combinations
Your domain models Payments as a strict aggregate.
External Models (Foreign, Untrusted)
public sealed class ExternalPaymentV1
{
public string PaymentId { get; init; } = default!;
public string Status { get; init; } = default!;
public decimal Amount { get; init; }
}
public sealed class ExternalPaymentV2
{
public string Id { get; init; } = default!;
public string State { get; init; } = default!;
public MoneyDto Money { get; init; } = default!;
}
These models are never visible to the domain.
Domain Model (Protected)
public sealed class Payment
{
public PaymentId Id { get; }
public PaymentStatus Status { get; }
public Money Amount { get; }
private Payment(PaymentId id, PaymentStatus status, Money amount)
{
Id = id;
Status = status;
Amount = amount;
}
public static Payment Create(
PaymentId id,
PaymentStatus status,
Money amount)
{
if (amount.Value <= 0)
throw new InvalidOperationException("Amount must be positive");
return new Payment(id, status, amount);
}
}
Inbound ACL Translators (Version-Specific)
public sealed class PaymentV1InboundTranslator
: IInboundTranslator<ExternalPaymentV1, Payment>
{
public TranslationResult<Payment> Translate(ExternalPaymentV1 input)
{
if (!PaymentStatus.TryFromExternal(input.Status, out var status))
{
return TranslationResult<Payment>.Failure(
new TranslationIssue(
"UnsupportedStatus",
$"Status '{input.Status}' is not supported"));
}
if (input.Amount <= 0)
{
return TranslationResult<Payment>.Failure(
new TranslationIssue(
"InvalidAmount",
"Amount must be greater than zero"));
}
return TranslationResult<Payment>.Success(
Payment.Create(
PaymentId.From(input.PaymentId),
status,
Money.From(input.Amount)));
}
}
public sealed class PaymentV2InboundTranslator
: IInboundTranslator<ExternalPaymentV2, Payment>
{
public TranslationResult<Payment> Translate(ExternalPaymentV2 input)
{
if (!PaymentStatus.TryFromExternal(input.State, out var status))
{
return TranslationResult<Payment>.Failure(
new TranslationIssue(
"UnsupportedState",
$"State '{input.State}' is not supported"));
}
return TranslationResult<Payment>.Success(
Payment.Create(
PaymentId.From(input.Id),
status,
Money.From(input.Money.Value)));
}
}
Each translator:
- Encodes exactly one external version
- Contains no branching on version
- Can be deleted safely
Version Routing (Orchestration Layer)
public sealed class PaymentIngressHandler
{
private readonly PaymentV1InboundTranslator _v1;
private readonly PaymentV2InboundTranslator _v2;
public PaymentIngressHandler(
PaymentV1InboundTranslator v1,
PaymentV2InboundTranslator v2)
{
_v1 = v1;
_v2 = v2;
}
public TranslationResult<Payment> Handle(
object payload,
int version)
{
return version switch
{
1 => _v1.Translate((ExternalPaymentV1)payload),
2 => _v2.Translate((ExternalPaymentV2)payload),
_ => TranslationResult<Payment>.Failure(
new TranslationIssue(
"UnsupportedVersion",
$"Version '{version}' is not supported"))
};
}
}
The domain is completely unaware of:
- External models
- Versions
- Translation failures
Composition with Domain Services
Note: The following examples use a simplified Payment concept to illustrate architectural boundaries.
The CapturePaymentUseCase class is to be found in the application layer. If it has multiple steps then you should call it a CapturePaymentSaga and follow the Saga pattern.
Real-world systems often model intent more explicitly (e.g. DisbursementInstruction) and delegate execution to external engines. The architectural principles remain the same.
public sealed class CapturePaymentUseCase
{
private readonly PaymentIngressHandler _ingress;
private readonly PaymentService _paymentService;
public CapturePaymentUseCase(
PaymentIngressHandler ingress,
PaymentService paymentService)
{
_ingress = ingress;
_paymentService = paymentService;
}
public void Execute(object payload, int version)
{
var result = _ingress.Handle(payload, version);
if (!result.IsSuccess)
{
// Reject / dead-letter / alert
return;
}
_paymentService.Capture(result.Value!);
}
}
This class does not:
enforce business invariants
contain domain rules
decide what “Capture” means
Instead, it:
coordinates a single user/system intent
wires together ACL + domain
controls whether the domain is invoked
handles success vs failure paths
public sealed class CapturePaymentSaga
{
private readonly PaymentIngressHandler _ingress;
private readonly PaymentService _paymentService;
// Infrastructure collaborators
private readonly IDeadLetterPublisher _deadLetterPublisher;
private readonly IMetricsSink _metrics;
private readonly IPaymentCompensator _compensator;
private readonly IOperationsNotifier _opsNotifier;
public CapturePaymentSaga(
PaymentIngressHandler ingress,
PaymentService paymentService,
IDeadLetterPublisher deadLetterPublisher,
IMetricsSink metrics,
IPaymentCompensator compensator,
IOperationsNotifier opsNotifier)
{
_ingress = ingress;
_paymentService = paymentService;
_deadLetterPublisher = deadLetterPublisher;
_metrics = metrics;
_compensator = compensator;
_opsNotifier = opsNotifier;
}
public void Execute(object payload, int version)
{
// STEP 1 — Translate (ACL)
var translation = _ingress.Handle(payload, version);
if (!translation.IsSuccess)
{
HandleTranslationFailure(translation);
return;
}
var payment = translation.Value!;
// STEP 2 — Domain behavior
try
{
_paymentService.Capture(payment);
}
catch (Exception ex)
{
HandleDomainFailure(payment, ex);
return;
}
// STEP 3 — Saga completes successfully
_metrics.RecordSuccess();
}
// ─────────────────────────────
// FAILURE OWNERSHIP (IN THE SAGA)
// ─────────────────────────────
private void HandleTranslationFailure(
TranslationResult<Payment> failure)
{
_deadLetterPublisher.Publish(failure);
_metrics.RecordIngressFailure();
}
private void HandleDomainFailure(
Payment payment,
Exception exception)
{
_compensator.Compensate(payment);
_opsNotifier.Notify(exception);
_metrics.RecordDomainFailure();
}
}
15. ACL Anti-Patterns
These are design failures, not stylistic disagreements.
- Domain services accepting DTOs
- One translator handling multiple versions
- Version flags inside domain logic
- AutoMapper or reflection-based translation
- ACLs performing IO or retries
- Shared contracts with external systems
If you see any of these, the ACL has failed.
16. Architectural Enforcement & Tests
ACL boundaries must be enforced automatically.
Example Rules
- Domain does not reference Integration
- Domain does not reference ACL
- ACL references Domain only
Example Architectural Tests (Conceptual)
- Assert no dependency from
Optima.Net.Domain→Integration - Assert no dependency from
Optima.Net.Domain→Acl - Assert no external DTO types appear in domain assemblies
If these tests fail, the architecture is already compromised.
17. Application Layer Primitives
The Application namespace defines control flow. It is the home of Use Cases and Sagas.
If the Domain answers “Is this allowed?” and the ACL answers “What does this mean?” then the Application layer answers:
- What should the system do next?”
This namespace exists to make system behaviour explicit, auditable, and testable.
Intent
express system intent explicitly
coordinate ACLs, domain logic, and outcomes
own flow control, sequencing, and stopping conditions
make use cases and sagas visible
prevent orchestration logic from leaking into:
controllers
message handlers
domain services
ACLs
The Application layer coordinates. It never decides business truth.
Explicit Non-Goals
The Application namespace does not:
enforce business rules (Policies do that)
evaluate facts (Specifications do that)
translate external models (ACL does that)
perform IO
call databases, queues, HTTP APIs, or schedulers
decide domain correctness
If any of these appear here, the boundary is already broken.
Core Concepts
There are exactly two executable primitives:
Use Case
Saga
And one semantic marker:
- Compensating Saga
Nothing else is required.
18. UseCase Example
A Use Case represents a single, synchronous application intent.
Characteristics:
single entry point
deterministic
no retries
no compensation
completes or rejects immediately
Mental model
“Given this input, do we proceed or not?”
Minimal Contract
namespace Optima.Net.Domain.Application;
public interface IUseCase<in TRequest, out TResult>
{
TResult Execute(TRequest request);
}
There is no async variant by default. Async is a transport concern, not a modeling concern.
Realistic Example – Create Disbursement Instruction
public sealed class CreateDisbursementInstructionUseCase
: IUseCase<CreateDisbursementRequest, ApplicationResult>
{
private readonly IInboundTranslator<CreateDisbursementDto, DisbursementInstruction> _translator;
private readonly DisbursementPolicy _policy;
private readonly DisbursementRepository _repository;
public CreateDisbursementInstructionUseCase(
IInboundTranslator<CreateDisbursementDto, DisbursementInstruction> translator,
DisbursementPolicy policy,
DisbursementRepository repository)
{
_translator = translator;
_policy = policy;
_repository = repository;
}
public ApplicationResult Execute(CreateDisbursementRequest request)
{
// Step 1 – Translate meaning (ACL)
var translation = _translator.Translate(request.Payload);
if (!translation.IsSuccess)
return ApplicationResult.Rejected(translation.Issues);
var instruction = translation.Value!;
// Step 2 – Ask the domain if we may proceed
if (!_policy.IsSatisfiedBy(instruction))
return ApplicationResult.Rejected("Disbursement policy not satisfied");
// Step 3 – Persist intent
_repository.Save(instruction);
return ApplicationResult.Completed();
}
}
Notice the following Key observations:
No business rules here
No IO except through collaborators
No retries
No workflow branching
Pure orchestration
19. Saga Example
Saga
A Saga represents a multi-step or failure-aware process.
Characteristics:
multiple steps
may span time
owns failure semantics
may retry, compensate, escalate, or stop
does not return a value
Mental model
“How do we try to make this happen safely?”
Minimal Contract
namespace Optima.Net.Domain.Application;
public interface ISaga<in TInput>
{
void Execute(TInput input);
}
If a Saga returns a value, it is lying. Outcomes are observed via state, decisions, or events.
Realistic Example – Execute Disbursement Saga
public sealed class ExecuteDisbursementSaga
: ISaga<DisbursementInstruction>, ICompensatingSaga
{
private readonly PaymentsEngineGateway _payments;
private readonly DisbursementRepository _repository;
public ExecuteDisbursementSaga(
PaymentsEngineGateway payments,
DisbursementRepository repository)
{
_payments = payments;
_repository = repository;
}
public void Execute(DisbursementInstruction instruction)
{
try
{
instruction.MarkInProgress();
foreach (var line in instruction.Lines)
{
_payments.Execute(line);
instruction.MarkLineCompleted(line);
}
instruction.MarkCompleted();
_repository.Save(instruction);
}
catch (Exception ex)
{
Compensate(instruction, ex);
throw;
}
}
private void Compensate(
DisbursementInstruction instruction,
Exception exception)
{
instruction.MarkFailed(exception.Message);
_repository.Save(instruction);
// Best-effort rollback / escalation
// No assumption of full reversibility
}
}
Key observations:
Compensation is explicit
Compensation is contextual
No fake symmetry
The Saga owns failure semantics
Infrastructure is called, but not owned
** Compensating Saga Marker**
Some Sagas include explicit compensation logic.
To make that intent visible and enforceable, Optima provides a marker interface.
public interface ICompensatingSaga { }
Design Rule
If a Saga contains compensation or reversal logic, it must implement ICompensatingSaga.
This is:
a semantic signal
enforceable via code review or analyzers
intentionally behavior-free
There is no Compensate() method on the interface. Compensation is designed explicitly, not abstracted.
Application Results vs Domain Decisions
Application outcomes describe flow, not truth.
Domain Decisions explain why
Application Results explain what happened next
Examples:
Completed
Rejected
Deferred
Failed
Do not reuse domain decisions as application outcomes.
Dependency Rules (Non-Negotiable)
Allowed:
Application -> Policies
Application -> Specifications
Application -> Decisions
Application -> ACL
ACL -> Domain
Not Allowed:
Domain -> Application
ACL -> Application
Application -> Infrastructure
Application enforcing business rules
Anti-Patterns
- Controllers orchestrating flow
- Message handlers containing business branches
- Domain services deciding next steps
- ACLs retrying or compensating
- “God” UseCases with multiple responsibilities
- Naming everything a Saga “for consistency”
20. Design Philosophy and Boundaries
Optima.Net.Domain intentionally avoids:
- Rule engines
- Workflow engines
- Infrastructure concerns
- Repositories or persistence abstractions
Its purpose is singular:
Make domain intent explicit, enforceable, and auditable.
If a rule matters, model it as a specification.
If a decision matters, model it as a policy.
If an integration matters, protect it with an ACL.
Mental Model Summary
Specifications → Is this true?
Policies → May we proceed?
Decisions → What happened, and why?
Application → What should we do next?
ACL → What does this external thing mean?
© Optima.Net.Domain
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Optima.Net.Domain:
| Package | Downloads |
|---|---|
|
Optima.Net.NegotiatR
NegotiatR is a deterministic, single-pass negotiation engine that proposes alternative intents when policies fail. It consumes policy diagnostics, never re-evaluates rules, and guarantees outcomes. Designed for domain-driven systems, it keeps fallback decisions explicit, auditable, and separate from policy evaluation, workflows, and execution logic, without retries or implicit control flow. |
GitHub repositories
This package is not used by any popular GitHub repositories.
RELEASENOTES.md