Architecture & Boundaries · Lesson 16

Capstone a bounded reservation architecture

Compose domain transitions, application ports, in-memory persistence, and graph checks honestly.

External .NET 10 SDK; five real projects with explicit ProjectReference items. Not executed by the single-file .NET 9 playground.

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

The capstone combines a bounded domain transition, an application-owned persistence port, explicit host composition, and real project-reference checks. InventoryState produces a candidate reservation state only for a positive quantity within available capacity. ReservationWorkflow prepares a corresponding notice and asks the store to commit against the observed version. MemoryReservationStore supplies a small sequential implementation for inspection. It is not a durable database, synchronized compare-and-swap primitive, or transactional outbox. The portable program demonstrates the local contract, while the external multi-project solution makes its physical dependencies and checker executable with a genuine .NET SDK.

Walk through the portable program

The initial state has version zero and three available units. Reserving two produces a candidate with one available unit and the next version; the memory store accepts it and records one notice. A second request for two is excessive and produces no candidate or commit. Reserving the final one succeeds, leaving zero and a second notice. Output is first:True, excess:False, last:True, remaining:0, and outbox:2. The rejection's lack of another notice matters independently of its false result; both state and publication intent should be observed when testing the workflow.

Build real physical dependencies

The external Domain assembly contains immutable state and notice values, Application contains the workflow and store contract, and Infrastructure contains the memory implementation. Host composes them. ArchitectureCheck reads the actual project files and verifies their inward policy, while the ordinary host output verifies the bounded scenario. This separation prevents a single-file demonstration from being mislabeled as an assembly-isolation test. A real storage adapter would need to implement a deliberate atomic conditional update and durable state-plus-message policy; the interface and version field merely identify where that responsibility belongs.

Pitfalls and unproved guarantees

The version comparison is sequential here and must not be advertised as thread-safe or cluster-safe. The memory method performs several ordinary operations and has no transaction or crash recovery. It also lacks a full idempotency protocol for retried client commands and no dispatcher delivers the recorded notices. Public storage contracts need clear trusted-input and failure assumptions; matching a version alone is not a substitute for validating arbitrary transitions. These omissions are explicit boundaries for further work, not capabilities secretly supplied by project separation or a collection named outbox.

Practice one: isolate behavior

First test zero and an excessive quantity on fresh valid stores and predict false with unchanged available units and no notices. Then reserve exactly all available units and predict true with zero remaining and one notice. Hint: inspect both the state snapshot and the outbox snapshot after each operation, keeping fixtures independent. Add a direct stale-version commit experiment to establish the memory adapter's stated comparison behavior without claiming that sequential evidence proves atomicity under real concurrent access.

Practice two: test a boundary

For another exercise, design a genuine database adapter with an expected-version condition, atomic state and outbox persistence, and a stable operation identity for retries. Hint: specify conflict, duplicate, and unavailable-store outcomes before choosing a library. Identify the integration tests needed against the real provider, including competing writers and process failure. Separately mutate the external Domain project's references and verify the architecture checker fails. The storage behavior tests and physical dependency check answer different questions and should retain separate expectations and evidence.

Review the evidence

A capstone review should trace one accepted request and one rejection through domain decision, application orchestration, storage boundary, and observable effects. Then inspect the real assembly graph and identify every guarantee not implemented by the memory adapter. The sample is complete for its bounded sequential purpose and intentionally honest about production gaps. Clean, Onion, Hexagonal, CQRS, and modular approaches can inform such a design without mandating a framework or microservice deployment. The useful architecture is the one whose responsibilities, dependencies, correctness boundaries, and operational assumptions remain explicit enough to test and change.

External .NET 10 SDK; five real projects with explicit ProjectReference items. Not executed by the single-file .NET 9 playground.

Capstone a bounded reservation architecture — physical dependency lab

Set up the SDK project

  1. Save every file at its exact relative path under a fresh directory.
  2. Use a .NET 10 SDK. Run dotnet build ArchitectureLab.slnx.
  3. Run dotnet run --project Host/Host.csproj --no-build.
  4. From the solution directory run dotnet run --project ArchitectureCheck/ArchitectureCheck.csproj --no-build -- .

Download complete SDK project ↗

Project files

ArchitectureLab.slnxDownload 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.csprojDownload 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.csprojDownload 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.csprojDownload 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.csprojDownload 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.csprojDownload 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.csDownload file ↗
using System;
using System.Collections.Generic;
using System.Linq;

namespace BoundaryLab.Domain;

public record InventoryState(int Version, int Available)
{
    public InventoryState? Reserve(int units) => units < 1 || units > Available ? null : new(checked(Version + 1), Available - units);
}
public record ReservationNotice(int Version, int Units);
Application/Model.csDownload file ↗
using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;

namespace BoundaryLab.Application;

public interface IReservationStore
{
    InventoryState Current { get; }
    bool TryCommit(InventoryState expected, InventoryState next, ReservationNotice notice);
}
public sealed class ReservationWorkflow(IReservationStore store)
{
    public bool Reserve(int units)
    {
        var before = store.Current;
        var after = before.Reserve(units);
        return after is not null && store.TryCommit(before, after, new ReservationNotice(after.Version, units));
    }
}
Infrastructure/Model.csDownload file ↗
using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
using BoundaryLab.Application;

namespace BoundaryLab.Infrastructure;

public sealed class MemoryReservationStore(InventoryState initial) : IReservationStore
{
    private readonly List<ReservationNotice> outbox = new();
    public InventoryState Current { get; private set; } = initial;
    public IReadOnlyList<ReservationNotice> Outbox => outbox.ToArray();
    public bool TryCommit(InventoryState expected, InventoryState next, ReservationNotice notice)
    {
        if (Current.Version != expected.Version) return false;
        Current = next; outbox.Add(notice); return true;
    }
}
Host/Program.csDownload file ↗
using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
using BoundaryLab.Application;
using BoundaryLab.Infrastructure;

var store = new MemoryReservationStore(new InventoryState(0, 3));
var service = new ReservationWorkflow(store);
Console.WriteLine($"first:{service.Reserve(2)}");
Console.WriteLine($"excess:{service.Reserve(2)}");
Console.WriteLine($"last:{service.Reserve(1)}");
Console.WriteLine($"remaining:{store.Current.Available}");
Console.WriteLine($"outbox:{store.Outbox.Count}");
ArchitectureCheck/Program.csDownload 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: first:True excess:False last:True remaining:0 outbox:2 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 MemoryReservationStore(new InventoryState(0, 3));
var service = new ReservationWorkflow(store);
Console.WriteLine($"first:{service.Reserve(2)}");
Console.WriteLine($"excess:{service.Reserve(2)}");
Console.WriteLine($"last:{service.Reserve(1)}");
Console.WriteLine($"remaining:{store.Current.Available}");
Console.WriteLine($"outbox:{store.Outbox.Count}");

public record InventoryState(int Version, int Available)
{
    public InventoryState? Reserve(int units) => units < 1 || units > Available ? null : new(checked(Version + 1), Available - units);
}
public record ReservationNotice(int Version, int Units);

public interface IReservationStore
{
    InventoryState Current { get; }
    bool TryCommit(InventoryState expected, InventoryState next, ReservationNotice notice);
}
public sealed class ReservationWorkflow(IReservationStore store)
{
    public bool Reserve(int units)
    {
        var before = store.Current;
        var after = before.Reserve(units);
        return after is not null && store.TryCommit(before, after, new ReservationNotice(after.Version, units));
    }
}

public sealed class MemoryReservationStore(InventoryState initial) : IReservationStore
{
    private readonly List<ReservationNotice> outbox = new();
    public InventoryState Current { get; private set; } = initial;
    public IReadOnlyList<ReservationNotice> Outbox => outbox.ToArray();
    public bool TryCommit(InventoryState expected, InventoryState next, ReservationNotice notice)
    {
        if (Current.Version != expected.Version) return false;
        Current = next; outbox.Add(notice); return true;
    }
}
Open C# practice

C# practice output

first:True
excess:False
last:True
remaining:0
outbox:2
Try this
Verify zero/excess requests cause no state or outbox change. Version comparison models a policy boundary; this single-thread memory adapter is not a concurrent durable database.

Further reading