Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.
The architectural boundary
A domain event records a meaningful fact in the model, while an outbox boundary concerns reliably persisting publication intent alongside a state change. This example first creates an immutable OrderTransition containing an accepted state and an OrderAccepted event. AcceptOrder passes both through one application-owned commit port. MemoryOrderCommit records them in lists solely to expose the intended collaboration. Those lists are not a transactional database or durable message broker. The portable program demonstrates the bounded behavior; the real external projects demonstrate dependency direction, not a production outbox's reliability guarantees.
Walk through the portable program
The supplied order has identifier A1, two units, and Accepted false. Its Accept operation produces a new state with Accepted true and a corresponding event. AcceptOrder sends that transition to the store and returns true. The memory adapter records one state and one event, so output is accepted:True, stored:1, and outbox:1. The original immutable input is not rewritten by the transition. Reading the adapter's snapshots makes the two local records observable, but matching list counts alone does not establish atomic persistence, crash recovery, or eventual remote delivery.
Build real physical dependencies
Domain owns the state and event values, Application owns the commit conversation, and Infrastructure supplies the memory implementation. The external solution enforces those source relationships physically. A real transactional outbox adapter must persist business state and the outbox record within an appropriate atomic storage transaction. A separate delivery mechanism then handles publication, retries, and duplicate processing according to its own contract. The sample deliberately has neither a database package nor a dispatcher. An interface named Commit states a desired boundary; its name cannot grant transactional guarantees to an implementation that only mutates ordinary lists.
Pitfalls and unproved guarantees
There is no duplicate-command protection in this baseline. Passing the same original unaccepted value again can append another transition because the use case does not load persisted identity or enforce a unique message key. Passing an already accepted state is a different input and returns false. Keep those scenarios distinct. The two list writes are also not atomic under arbitrary failure, and state disappears on process restart. Domain events, integration events, durable outbox records, and broker deliveries have related but different roles; do not collapse them into an invented exactly-once guarantee.
Practice one: isolate behavior
First pass a state whose Accepted value is already true and predict false with both adapter collections still empty on a fresh fixture. Then pass a zero-unit state and predict the same no-commit outcome. Hint: arrange each rejection independently so prior accepted records cannot confuse the counts. Add a valid three-unit input and inspect the stored immutable state and event values directly, not just their counts. These cases test transition and orchestration behavior without claiming that a memory collection established durable transaction semantics.
Practice two: test a boundary
For another exercise, call the use case twice with the same original unaccepted input and observe the duplicate records. Hint: this exposes a missing idempotency policy rather than a failure of immutable records. Design a real persistence contract with a stable operation identity, a unique constraint, and atomic state-plus-outbox storage; identify which failures need integration tests against the actual provider. Then outline a dispatcher that tolerates retries and a consumer that handles duplicate delivery. Keep that proposed architecture separate from the executable lists, which do not implement those capabilities.
Review the evidence
Review event publication by locating the domain transition, commit boundary, storage guarantees, delivery owner, and duplicate-handling policy. The sample's three lines establish one local accepted transition, while the external graph establishes where those responsibilities depend on each other. Production reliability requires more evidence than either. Use the outbox pattern when its transaction and delivery model fits the system, and verify it against real storage behavior. A clearly labeled bounded model is useful for reasoning; claiming its lists are a durable outbox would erase precisely the failure boundaries the architecture is meant to clarify.
Domain events and the outbox commit boundary — physical dependency lab
Set up the SDK project
- Save every file at its exact relative path under a fresh directory.
- Use a .NET 10 SDK. Run dotnet build ArchitectureLab.slnx.
- Run dotnet run --project Host/Host.csproj --no-build.
- From the solution directory run dotnet run --project ArchitectureCheck/ArchitectureCheck.csproj --no-build -- .
Download complete SDK project ↗
Project files
ArchitectureLab.slnx
Download file ↗<Solution>
<Project Path="Domain/Domain.csproj" />
<Project Path="Application/Application.csproj" />
<Project Path="Infrastructure/Infrastructure.csproj" />
<Project Path="Host/Host.csproj" />
<Project Path="ArchitectureCheck/ArchitectureCheck.csproj" />
</Solution>
Domain/Domain.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Application/Application.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../Domain/Domain.csproj" />
</ItemGroup>
</Project>
Infrastructure/Infrastructure.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../Domain/Domain.csproj" />
<ProjectReference Include="../Application/Application.csproj" />
</ItemGroup>
</Project>
Host/Host.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../Domain/Domain.csproj" />
<ProjectReference Include="../Application/Application.csproj" />
<ProjectReference Include="../Infrastructure/Infrastructure.csproj" />
</ItemGroup>
</Project>
ArchitectureCheck/ArchitectureCheck.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Domain/Model.cs
Download file ↗using System;
using System.Collections.Generic;
using System.Linq;
namespace BoundaryLab.Domain;
public record OrderAccepted(string Id, int Units);
public record OrderTransition(OrderState State, OrderAccepted Event);
public record OrderState(string Id, int Units, bool Accepted)
{
public OrderTransition? Accept() => Accepted || Units < 1 ? null : new(this with { Accepted = true }, new OrderAccepted(Id, Units));
}
Application/Model.cs
Download file ↗using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
namespace BoundaryLab.Application;
public interface IOrderCommit { void Commit(OrderState state, OrderAccepted message); }
public sealed class AcceptOrder(IOrderCommit store)
{
public bool Execute(OrderState state)
{
var transition = state.Accept();
if (transition is null) return false;
store.Commit(transition.State, transition.Event); return true;
}
}
Infrastructure/Model.cs
Download file ↗using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
using BoundaryLab.Application;
namespace BoundaryLab.Infrastructure;
public sealed class MemoryOrderCommit : IOrderCommit
{
private readonly List<OrderState> orders = new();
private readonly List<OrderAccepted> outbox = new();
public IReadOnlyList<OrderState> Orders => orders.ToArray();
public IReadOnlyList<OrderAccepted> Outbox => outbox.ToArray();
public void Commit(OrderState state, OrderAccepted message) { orders.Add(state); outbox.Add(message); }
}
Host/Program.cs
Download file ↗using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
using BoundaryLab.Application;
using BoundaryLab.Infrastructure;
var store = new MemoryOrderCommit();
Console.WriteLine($"accepted:{new AcceptOrder(store).Execute(new OrderState("A1", 2, false))}");
Console.WriteLine($"stored:{store.Orders.Count}");
Console.WriteLine($"outbox:{store.Outbox.Count}");
ArchitectureCheck/Program.cs
Download file ↗using System.Xml.Linq;
string root = Path.GetFullPath(args.Length == 0 ? "." : args[0]);
var allowed = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal)
{
["Domain"] = new(),
["Application"] = new() { "Domain" },
["Infrastructure"] = new() { "Domain", "Application" },
["Host"] = new() { "Domain", "Application", "Infrastructure" }
};
int violations = 0;
foreach (var item in allowed)
{
string path = Path.Combine(root, item.Key, item.Key + ".csproj");
var document = XDocument.Load(path);
foreach (var reference in document.Descendants().Where(element => element.Name.LocalName == "ProjectReference"))
{
string include = (string?)reference.Attribute("Include") ?? throw new InvalidOperationException("Missing Include");
string targetPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path)!, include));
string target = Path.GetFileNameWithoutExtension(targetPath);
string expectedPath = Path.Combine(root, target, target + ".csproj");
if (!item.Value.Contains(target) || targetPath != expectedPath)
{
violations++;
Console.WriteLine($"violation:{item.Key}->{target}");
}
}
}
Console.WriteLine($"projects:{allowed.Count}");
Console.WriteLine($"violations:{violations}");
return violations == 0 ? 0 : 1;
Expected result
Host output: accepted:True stored:1 outbox:1 Physical ProjectReference policy check output: projects:4 violations:0 Both commands exit zero. The checker inspects the four business-project files; its own tooling project is outside that policy. It checks declared project-reference direction, not all runtime behavior or package dependencies.
Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.
C# companion practice
using System;
using System.Collections.Generic;
using System.Linq;
var store = new MemoryOrderCommit();
Console.WriteLine($"accepted:{new AcceptOrder(store).Execute(new OrderState("A1", 2, false))}");
Console.WriteLine($"stored:{store.Orders.Count}");
Console.WriteLine($"outbox:{store.Outbox.Count}");
public record OrderAccepted(string Id, int Units);
public record OrderTransition(OrderState State, OrderAccepted Event);
public record OrderState(string Id, int Units, bool Accepted)
{
public OrderTransition? Accept() => Accepted || Units < 1 ? null : new(this with { Accepted = true }, new OrderAccepted(Id, Units));
}
public interface IOrderCommit { void Commit(OrderState state, OrderAccepted message); }
public sealed class AcceptOrder(IOrderCommit store)
{
public bool Execute(OrderState state)
{
var transition = state.Accept();
if (transition is null) return false;
store.Commit(transition.State, transition.Event); return true;
}
}
public sealed class MemoryOrderCommit : IOrderCommit
{
private readonly List<OrderState> orders = new();
private readonly List<OrderAccepted> outbox = new();
public IReadOnlyList<OrderState> Orders => orders.ToArray();
public IReadOnlyList<OrderAccepted> Outbox => outbox.ToArray();
public void Commit(OrderState state, OrderAccepted message) { orders.Add(state); outbox.Add(message); }
}
C# practice output
accepted:True stored:1 outbox:1
Use an already-accepted input to verify no new commit. The lists model intent; they are not a durable transactional outbox or a message-delivery guarantee.