The most hazardous trap in evolving a monolithic architecture is not business complexity, but unchecked dependency erosion. In many engineering teams, developers seeking quick implementation shortcuts inject internal entities or write cross-module SQL JOIN queries directly across domain boundaries. Within a year, the system degenerates into an unmaintainable “Big Ball of Mud,” forcing expensive refactoring initiatives.
BitzOrcas.Modern treats “executable architectural constraints” as a core design principle. Through physical assembly isolation, autonomous IAppModule governance manifests, and ArchUnitNET architectural unit tests integrated into CI gates, each domain module operates with the autonomy of a microservice while retaining the sub-millisecond in-process execution and unified debugging of a monolith.
This tutorial guides you through creating a production-grade Legal Contract Management (LegalContract) module from project scaffolding to architecture test validation.
Physical Architecture & Dependency Rules
In BitzOrcas.Modern, every business module is partitioned into three strictly unidirectional physical projects:
Step 1: Establish Physical Projects and Directory Layout
Create an isolated directory structure under src/Modules/Business/:
src/Modules/Business/LegalContract/├── BitzOrcas.Modules.LegalContract.Contracts/ # Public contract layer (consumed by external modules, zero implementations)│ ├── Events/ # Cross-module integration event contracts│ ├── Queries/ # Cross-module read-only query port contracts│ └── Dtos/ # Data transfer objects (DTOs)├── BitzOrcas.Modules.LegalContract.Domain/ # Domain kernel layer (pure state and business rules)│ ├── Aggregates/ # Unified aggregate roots inheriting TenantAggregateRoot│ └── ValueObjects/ # Strongly typed value objects and domain enumerations└── BitzOrcas.Modules.LegalContract.Application/ # Application implementation layer (vertical slice execution) ├── Commands/ # Vertical slice write models (One File Use Case) ├── Queries/ # Vertical slice read models (high-throughput projections) └── LegalContractModule.cs # Module governance descriptor (implementing IAppModule)Step 2: Define Cross-Module Public Contracts
Define public integration events and read-only query contracts inside BitzOrcas.Modules.LegalContract.Contracts:
using BitzOrcas.Domain.Contracts;
namespace BitzOrcas.Modules.LegalContract.Contracts.Events;
/// <summary>/// Cross-module integration event emitted upon contract execution./// </summary>/// <remarks>/// Staged atomically into the CAP Transactional Outbox table during local database commit./// Relayed by the background daemon to RabbitMQ for billing quota deductions./// </remarks>public sealed record ContractSignedIntegrationEvent( string ContractId, string ContractCode, string TenantId, string ClientName, decimal TotalAmount, DateTimeOffset SignedAtUtc) : IDomainEvent;Step 3: Implement the Domain Unified Aggregate Root
In BitzOrcas.Modules.LegalContract.Domain, implement the aggregate root inheriting TenantAggregateRoot<string>:
using System;using System.ComponentModel;using BitzOrcas.Domain.Entities;using BitzOrcas.Domain.Results;using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Modules.LegalContract.Domain.Aggregates;
/// <summary>/// Static domain error catalog for legal contracts./// </summary>public static class ContractErrors{ /// <summary> /// Contract total amount is invalid /// </summary> public static readonly Error InvalidAmount = Error.Validation("Contract.InvalidAmount", "Contract monetary amount cannot be negative.");
/// <summary> /// Contract current state does not permit the requested operation /// </summary> public static readonly Error InvalidState = Error.Conflict("Contract.InvalidState", "Only contracts in Draft status may be signed.");}
/// <summary>/// Legal contract unified aggregate root./// </summary>[BitzTable("LegalContract", IsTenant = true, IsSoftDelete = true, Description = "Enterprise Legal Contracts Ledger")]public sealed class Contract : TenantAggregateRoot<string>{ private const int CodeMaxLength = 32; private const int TitleMaxLength = 200;
[BitzColumn(Length = CodeMaxLength, IsRequired = true, IsUnique = true, Description = "Contract Tracking Code")] public string ContractCode { get; private set; } = string.Empty;
[BitzColumn(Length = TitleMaxLength, IsRequired = true, Description = "Contract Title")] public string Title { get; private set; } = string.Empty;
[BitzColumn(Precision = 18, Scale = 2, IsRequired = true, Description = "Total Monetary Value")] public decimal TotalAmount { get; private set; }
[BitzColumn(IsRequired = true, Description = "Execution Lifecycle Status")] public ContractStatus Status { get; private set; } = ContractStatus.Draft;
/// <summary> /// Parameterless constructor reserved strictly for ORM materialization. /// </summary> [Obsolete("For ORM materialization only. Use Draft.", error: true)] [EditorBrowsable(EditorBrowsableState.Never)] public Contract() : base("0") { }
private Contract( string id, string contractCode, string title, decimal totalAmount, string tenantId) : base(id) { ContractCode = contractCode; Title = title; TotalAmount = totalAmount; TenantId = tenantId; Status = ContractStatus.Draft; }
/// <summary> /// Factory method: drafts a new contract. /// </summary> public static Result<Contract> Draft( string id, string contractCode, string title, decimal totalAmount, string tenantId) { if (totalAmount < 0) { return Result<Contract>.Failure(ContractErrors.InvalidAmount); }
var contract = new Contract(id, contractCode.Trim(), title.Trim(), totalAmount, tenantId); return Result<Contract>.Success(contract); }
/// <summary> /// Executes contract signing and state transition. /// </summary> public Result Sign() { if (Status != ContractStatus.Draft) { return Result.Failure(ContractErrors.InvalidState); }
Status = ContractStatus.Signed; return Result.Success(); }}
public enum ContractStatus{ Draft = 1, Signed = 2, Terminated = 3}Step 4: Implement Autonomous Module Governance (IAppModule)
Create LegalContractModule.cs in the Application layer, implementing IAppModule to declare boundaries and policies:
using BitzOrcas.Modularity;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;
namespace BitzOrcas.Modules.LegalContract.Application;
/// <summary>/// Legal contract module composition root and boundary declaration./// </summary>public sealed class LegalContractModule : IAppModule{ public string Name => "LegalContract";
public string BaseNamespace => "BitzOrcas.Modules.LegalContract";
/// <summary> /// Declared module dependencies. /// </summary> public IReadOnlyList<string> Dependencies => ["Identity", "Authorization"];
/// <summary> /// Exported integration events. /// </summary> public IReadOnlyList<string> PublishedEvents => [ "BitzOrcas.Modules.LegalContract.Contracts.Events.ContractSignedIntegrationEvent" ];
/// <summary> /// Subscribed external events. /// </summary> public IReadOnlyList<string> SubscribedEvents => [];
/// <summary> /// Publicly exposed contract namespaces. /// </summary> public IReadOnlyList<string> PublicContractNamespaces => [ "BitzOrcas.Modules.LegalContract.Contracts" ];
/// <summary> /// Permissions owned by this module. /// </summary> public IReadOnlyList<string> OwnedPermissions => [ "LegalContract.Contract.Create", "LegalContract.Contract.View", "LegalContract.Contract.Sign" ];
/// <summary> /// Registers custom business services and adapters. /// </summary> public void ConfigureServices(IServiceCollection services, IConfiguration configuration) { // Register module-specific adapters and policy providers }}Step 5: Enforcing Boundaries via ArchUnitNET Fitness Functions
Verbal guidelines cannot prevent architecture decay; automated tests must enforce rules continuously.
Add architectural test guards under tests/BitzOrcas.Architecture.Tests/:
using System.Reflection;using ArchUnitNET.Fluent;using ArchUnitNET.Loader;using Shouldly;using Xunit;using static ArchUnitNET.Fluent.ArchRuleDefinition;
public class LegalContractModuleBoundaryTests{ private static readonly Assembly ContractsAsm = Assembly.Load("BitzOrcas.Modules.LegalContract.Contracts"); private static readonly Assembly DomainAsm = Assembly.Load("BitzOrcas.Modules.LegalContract.Domain"); private static readonly Assembly ApplicationAsm = Assembly.Load("BitzOrcas.Modules.LegalContract.Application");
private static readonly ArchUnitNET.Domain.Architecture Architecture = new ArchLoader() .LoadAssemblies(ContractsAsm, DomainAsm, ApplicationAsm) .Build();
[Fact] public void Contracts_Should_Not_Reference_Domain_Or_Application() { // Fitness Function 1: Public contracts must never reference internal layers var references = ContractsAsm.GetReferencedAssemblies().Select(a => a.Name).ToArray();
references.ShouldNotContain("BitzOrcas.Modules.LegalContract.Domain"); references.ShouldNotContain("BitzOrcas.Modules.LegalContract.Application"); }
[Fact] public void External_Modules_Should_Only_Reference_Contracts() { // Fitness Function 2: External modules (e.g. Billing) must only depend on Contracts var rule = Types().That().ResideInNamespace("BitzOrcas.Modules.Billing..") .Should().NotDependOnAny( Types().That().ResideInNamespace("BitzOrcas.Modules.LegalContract.Domain..") .Or().ResideInNamespace("BitzOrcas.Modules.LegalContract.Application.."));
rule.Evaluate(Architecture).HasViolations.ShouldBeFalse(); }}Module Delivery Checklist
Verify all five engineering rules before opening a pull request:
- Physical projects cleanly partitioned across
Contracts,Domain, andApplication? - Aggregate roots inherit from
TenantAggregateRoot<TId>and specify[BitzTable]metadata? -
LegalContractModule.csdeclares all permissions and published integration events? - Cross-module synchronization occurs solely via CAP Outbox integration events or
Contractsread-only ports? -
dotnet test tests/BitzOrcas.Architecture.Testspasses with 100% green status?