Skip to content
bitzorcas
中EN

Tutorial

Creating a Business Module from Scratch: Building High-Cohesion Domains Like Lego

Follow BitzOrcas.Modern modular monolith standards to build a Legal Contract Management module from scratch: partitioning physical boundaries across Contracts/Domain/Application, declaring IAppModule governance metadata, inheriting TenantAggregateRoot, and guarding boundaries with ArchUnitNET fitness functions.

Last updated

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:

External Consumer Module (e.g., Billing)Ingress & Composition Root (API Host / AppHost)Allowed to reference publicContracts onlyDirect internalimplementation referenceforbiddenDirect internalimplementation referenceforbiddenBitzOrcas.Modules.LegalContract Business Module

1. LegalContract.Contracts
(Public interfaces, read-only DTOs, and integration event contracts)

3. LegalContract.Application
(Vertical slice use cases, IRequestRule validators, handlers, and IAppModule)

2. LegalContract.Domain
(Contract unified aggregate root, enums, value objects, and domain invariants)

BitzOrcas.Api (Route aggregation and DI wiring)

SaaS Billing Settlement Handler


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:

src/Modules/Business/LegalContract/BitzOrcas.Modules.LegalContract.Contracts/Events/ContractSignedIntegrationEvent.cs
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>:

src/Modules/Business/LegalContract/BitzOrcas.Modules.LegalContract.Domain/Aggregates/Contract.cs
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:

src/Modules/Business/LegalContract/BitzOrcas.Modules.LegalContract.Application/LegalContractModule.cs
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/:

tests/BitzOrcas.Architecture.Tests/LegalContractModuleBoundaryTests.cs
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, and Application?
  • Aggregate roots inherit from TenantAggregateRoot<TId> and specify [BitzTable] metadata?
  • LegalContractModule.cs declares all permissions and published integration events?
  • Cross-module synchronization occurs solely via CAP Outbox integration events or Contracts read-only ports?
  • dotnet test tests/BitzOrcas.Architecture.Tests passes with 100% green status?

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%