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
An architectural dependency is a source or assembly relationship, not simply the direction in which a method happens to call another object. QuoteService needs a rate at runtime, but it declares that need through IRateSource, an application-owned interface. FixedRate implements the interface outside the application layer. The host supplies that object explicitly. This arrangement lets stable policy avoid a source reference to its particular rate provider. The portable file demonstrates the call relationship; the complete external solution adds real project boundaries so the intended reference direction is enforceable rather than merely suggested by class names.
Walk through the portable program
The host creates FixedRate with rate three and passes it to QuoteService. Calling Total with two first checks the domain's positive-unit rule, then multiplies two by the supplied rate in checked integer arithmetic. The result is six, so the single output line is total:6. QuoteService invokes an object implemented by infrastructure at runtime, while its declared constructor type remains the inner-owned interface. This is the distinction to follow: runtime dispatch reaches the outer implementation without requiring the application source to name FixedRate or its assembly.
Build real physical dependencies
The external solution contains Domain, Application, Infrastructure, Host, and a separate ArchitectureCheck tool. Application references Domain; Infrastructure references the contracts it implements; Host performs composition using the public types. Domain has no business-project references. Each relationship is an actual ProjectReference item, and the checker reads those files rather than guessing from namespaces. Build the solution, run Host for the same expected value, then run the checker for four business projects with zero violations. Its scope is declared references, not proof that every possible runtime interaction follows an ideal design.
Pitfalls and unproved guarantees
A namespace or folder called Domain does not isolate code compiled into the same assembly. Conversely, a runtime interface call does not imply that compile-time references must point toward the implementation. Avoid creating an interface for every tiny helper merely to make a diagram look layered; identify a meaningful variation or boundary first. This bounded example uses a rate provider as that variation. It does not establish network resilience, persistence, distributed coordination, or measurable performance. Those concerns need real adapters, explicit failure contracts, and appropriate tests in addition to the dependency structure.
Practice one: isolate behavior
First replace FixedRate with another implementation returning five and predict total:10 without editing QuoteService. Hint: compose a different object at the host while preserving the same port. Then pass zero units and predict the domain validation exception before multiplication. Keep those two scenarios separate: swapping a provider tests the dependency seam, while invalid quantity tests a rule. A green result for one cannot stand in for the other, and changing an expected value merely to match the implementation would weaken the evidence.
Practice two: test a boundary
For another exercise, add a forbidden Domain reference to Infrastructure in a separate copy of the external solution and run ArchitectureCheck. Predict a named violation and a nonzero exit. Hint: the checker reads the physical project files, so changing only a namespace should not reproduce this result. Restore the valid graph afterward. Then remove a source using directive without altering references and observe that namespace visibility and assembly dependencies are separate mechanisms. Do not claim the portable single-file version performs the same assembly isolation check.
Review the evidence
Review dependency direction by listing the interface owner, implementation owner, composition root, and actual project references. Trace runtime calls separately so the two arrow types cannot be confused. The example's total establishes its small behavior, and the external build and graph check establish specific physical constraints. Neither requires a web framework, a dependency-injection container, or microservices. Choose architecture to make the application's meaningful policy and change boundaries clear, then keep those boundaries concrete enough that a build or a focused check can detect an accidental reference in the wrong direction.
Dependency direction versus runtime calls — 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 static class UnitRule { public static bool Valid(int units) => units > 0; }
Application/Model.cs
Download file ↗using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
namespace BoundaryLab.Application;
public interface IRateSource { int GetRate(); }
public sealed class QuoteService(IRateSource rates)
{
public int Total(int units)
{
if (!UnitRule.Valid(units)) throw new ArgumentOutOfRangeException(nameof(units));
return checked(units * rates.GetRate());
}
}
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 FixedRate(int rate) : IRateSource { public int GetRate() => rate; }
Host/Program.cs
Download file ↗using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
using BoundaryLab.Application;
using BoundaryLab.Infrastructure;
var quote = new QuoteService(new FixedRate(3));
Console.WriteLine($"total:{quote.Total(2)}");
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: total:6 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 quote = new QuoteService(new FixedRate(3));
Console.WriteLine($"total:{quote.Total(2)}");
public static class UnitRule { public static bool Valid(int units) => units > 0; }
public interface IRateSource { int GetRate(); }
public sealed class QuoteService(IRateSource rates)
{
public int Total(int units)
{
if (!UnitRule.Valid(units)) throw new ArgumentOutOfRangeException(nameof(units));
return checked(units * rates.GetRate());
}
}
public sealed class FixedRate(int rate) : IRateSource { public int GetRate() => rate; }
C# practice output
total:6
Replace FixedRate without changing QuoteService. The external checker verifies physical ProjectReference policy; the portable file cannot provide assembly isolation.