Skip to content
bitzorcas
中EN

Recipe

Practical Guide: Add an Autonomous Business Module

Master creating autonomous modular monolith sub-domains in BitzOrcas.Modern: Contracts/Domain/Application project segregation, TenantAggregateRoot domain models, IAppModule governance metadata, and ArchUnit architecture guardrails.

Last updated

In modular monolith architectures, when an entirely new business sub-domain emerges (e.g. Legal Knowledge Base and Precedent Judgments LegalKnowledge), development teams often succumb to convenient shortcuts: dumping new code into existing modules or pretending folder trees within a single project constitute “modules.” As domain complexity escalates, cross-domain entities and database tables become tightly coupled, inevitably degrading the monolith into an unmaintainable “Big Ball of Mud.”

BitzOrcas.Modern enforces “Physical Isolation, Contract Decoupling, and Compile-Time Governance”:

  1. Three-Tier Physical Project Segregation: Every deep module is decomposed into .Contracts, .Domain, and host implementation projects, preventing cross-cutting entanglement;
  2. Strict Unidirectional Contract Red Line: Cross-module synchronous communication is permitted strictly via the target’s .Contracts project. Referencing internal Domain, Application, or Infrastructure code across modules is strictly forbidden;
  3. Outbox-Driven Cross-Module State Changes: State propagation across module boundaries relies on transactional integration events powered by CAP, avoiding distributed transaction locks;
  4. IAppModule Declarative Governance Contract: Modules self-declare identity metadata, dependencies, owned RBAC action codes, feature toggles, and integration events for automated dependency graph analysis;
  5. Automated ArchUnit Boundary Guardrails: CI pipelines statically analyze assembly dependency topologies, immediately failing builds if unauthorized references are introduced.

This guide walks through building the Legal Knowledge Base and Case Precedent Management Module (BitzOrcas.Modules.LegalKnowledge) from scratch.

Module Physical Layout & Dependencies

Other Business Modules (e.g. Legal / Billing)BitzOrcas.Modules.LegalKnowledge ModuleAPI Host Composition Root (src/Hosts/BitzOrcas.Api)Permitted to reference onlyContracts

Program.cs (Static AddBitzModules)

1. BitzOrcas.Modules.LegalKnowledge.Contracts
(Public DTOs, Query Ports & Events)

2. BitzOrcas.Modules.LegalKnowledge.Domain
(Aggregate Roots, Enums & Invariants)

3. BitzOrcas.Modules.LegalKnowledge
(Slices, Handlers & IAppModule Root)

BitzOrcas.Modules.Legal


Step 1: Create 3 Standard Physical .csproj Projects

Establish three isolated projects under src/Modules/LegalKnowledge/:

1. BitzOrcas.Modules.LegalKnowledge.Contracts.csproj (Public Contract Tier)

Contains public DTOs, integration event records, and read-only query ports. External modules are allowed to reference only this project:

src/Modules/LegalKnowledge/BitzOrcas.Modules.LegalKnowledge.Contracts/BitzOrcas.Modules.LegalKnowledge.Contracts.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Framework\BitzOrcas.Domain\BitzOrcas.Domain.csproj" />
</ItemGroup>
</Project>

2. BitzOrcas.Modules.LegalKnowledge.Domain.csproj (Domain Kernel Tier)

Contains aggregate roots, domain enums, business invariants, and dual-ORM metadata attributes. External modules must never reference this project:

src/Modules/LegalKnowledge/BitzOrcas.Modules.LegalKnowledge.Domain/BitzOrcas.Modules.LegalKnowledge.Domain.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BitzOrcas.Modules.LegalKnowledge.Contracts\BitzOrcas.Modules.LegalKnowledge.Contracts.csproj" />
<ProjectReference Include="..\..\..\Framework\BitzOrcas.Persistence.Metadata\BitzOrcas.Persistence.Metadata.csproj" />
</ItemGroup>
</Project>

3. BitzOrcas.Modules.LegalKnowledge.csproj (Application Slices and Assembly Root)

Houses vertical slice handlers, Minimal API route bindings, and the IAppModule assembly definition:

src/Modules/LegalKnowledge/BitzOrcas.Modules.LegalKnowledge/BitzOrcas.Modules.LegalKnowledge.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BitzOrcas.Modules.LegalKnowledge.Domain\BitzOrcas.Modules.LegalKnowledge.Domain.csproj" />
<ProjectReference Include="..\..\..\Framework\BitzOrcas.Application\BitzOrcas.Application.csproj" />
<ProjectReference Include="..\..\..\Framework\BitzOrcas.Modularity\BitzOrcas.Modularity.csproj" />
</ItemGroup>
</Project>

Step 2: Implement the Domain Aggregate Root

Create KnowledgeDocument.cs inside BitzOrcas.Modules.LegalKnowledge.Domain. The entity inherits TenantAggregateRoot<string> and declares unified EF Core and SqlSugar metadata attributes via [BitzTable]:

src/Modules/LegalKnowledge/BitzOrcas.Modules.LegalKnowledge.Domain/KnowledgeDocument.cs
using System;
using System.ComponentModel;
using BitzOrcas.Domain.Entities;
using BitzOrcas.Domain.Results;
using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Modules.LegalKnowledge.Domain;
/// <summary>
/// Legal knowledge base document aggregate root
/// </summary>
/// <remarks>
/// <para>Inherits <see cref="TenantAggregateRoot{TId}"/>, equipping identity, tenant isolation, auditing, and concurrency fields.</para>
/// <para>Mapped to the <c>LegalKnowledgeDocument</c> database table.</para>
/// </remarks>
[BitzTable("LegalKnowledgeDocument", IsTenant = true, IsSoftDelete = true, Description = "Legal Knowledge Base and Case Precedent Table")]
[BitzIndex("UX_LegalKnowledge_Tenant_DocCode", "TenantId", "DocumentCode", IsUnique = true)]
public sealed class KnowledgeDocument : TenantAggregateRoot<string>
{
private const int CodeMaxLength = 64;
private const int TitleMaxLength = 200;
/// <summary>
/// Business tracking code for knowledge document
/// </summary>
[BitzColumn(Length = CodeMaxLength, IsRequired = true, Description = "Business tracking code")]
public string DocumentCode { get; private set; } = string.Empty;
/// <summary>
/// Document title
/// </summary>
[BitzColumn(Length = TitleMaxLength, IsRequired = true, Description = "Document title")]
public string DocumentTitle { get; private set; } = string.Empty;
/// <summary>
/// Classification tags (semicolon-delimited)
/// </summary>
[BitzColumn(Length = 256, Description = "Classification tags")]
public string Tags { get; private set; } = string.Empty;
/// <summary>
/// Document content in Markdown format
/// </summary>
[BitzColumn(Length = int.MaxValue, Description = "Markdown body content")]
public string ContentMarkdown { get; private set; } = string.Empty;
/// <summary>
/// Parameterless constructor strictly reserved for ORM deserialization
/// </summary>
[Obsolete("Reserved for ORM deserialization. Business logic must call the Create factory method.", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public KnowledgeDocument() : base("0") { }
private KnowledgeDocument(
string id,
string tenantId,
string documentCode,
string documentTitle,
string tags,
string contentMarkdown) : base(id)
{
TenantId = tenantId;
DocumentCode = documentCode;
DocumentTitle = documentTitle;
Tags = tags;
ContentMarkdown = contentMarkdown;
}
/// <summary>
/// Explicit factory method: constructs aggregate root while guarding domain invariants
/// </summary>
/// <param name="id">Aggregate identifier.</param>
/// <param name="tenantId">Tenant identifier.</param>
/// <param name="documentCode">Business tracking code.</param>
/// <param name="documentTitle">Document title.</param>
/// <param name="tags">Classification tags.</param>
/// <param name="contentMarkdown">Body content in Markdown.</param>
/// <returns>A Result containing the aggregate root or a domain validation error.</returns>
public static Result<KnowledgeDocument> Create(
string id,
string tenantId,
string documentCode,
string documentTitle,
string tags,
string contentMarkdown)
{
if (string.IsNullOrWhiteSpace(documentTitle))
{
return Result.Failure<KnowledgeDocument>(KnowledgeErrors.TitleRequired);
}
if (documentTitle.Trim().Length > TitleMaxLength)
{
return Result.Failure<KnowledgeDocument>(KnowledgeErrors.TitleTooLong);
}
if (string.IsNullOrWhiteSpace(documentCode))
{
return Result.Failure<KnowledgeDocument>(KnowledgeErrors.CodeRequired);
}
var doc = new KnowledgeDocument(
id: id,
tenantId: tenantId,
documentCode: documentCode.Trim().ToUpperInvariant(),
documentTitle: documentTitle.Trim(),
tags: tags?.Trim() ?? string.Empty,
contentMarkdown: contentMarkdown ?? string.Empty);
return Result.Success(doc);
}
}
/// <summary>
/// Typed error catalog for legal knowledge module
/// </summary>
public static class KnowledgeErrors
{
public static readonly Error TitleRequired = Error.Validation("Knowledge.TitleRequired", "Knowledge document title cannot be empty.");
public static readonly Error TitleTooLong = Error.Validation("Knowledge.TitleTooLong", "Knowledge document title cannot exceed 200 characters.");
public static readonly Error CodeRequired = Error.Validation("Knowledge.CodeRequired", "Knowledge document tracking code cannot be empty.");
}

Step 3: Implement Module Governance Metadata (IAppModule)

Implement IAppModule in the root of BitzOrcas.Modules.LegalKnowledge. This interface serves as the module’s “architectural passport,” allowing the composition root to generate dependency graphs, register RBAC action codes, and inject policy services:

src/Modules/LegalKnowledge/BitzOrcas.Modules.LegalKnowledge/LegalKnowledgeModule.cs
using System.Collections.Generic;
using BitzOrcas.Modularity;
using Microsoft.Extensions.DependencyInjection;
namespace BitzOrcas.Modules.LegalKnowledge;
/// <summary>
/// Legal knowledge base module declaration and assembly root
/// </summary>
/// <remarks>
/// <para>Implements <see cref="IAppModule"/> for compile-time topological discovery and permission registration.</para>
/// <para>Red line: External modules may reference only public Contracts namespaces.</para>
/// </remarks>
public sealed class LegalKnowledgeModule : IAppModule
{
/// <summary>
/// Unique module identifier
/// </summary>
public string Name => "LegalKnowledge";
/// <summary>
/// Root namespace of the module
/// </summary>
public string BaseNamespace => "BitzOrcas.Modules.LegalKnowledge";
/// <summary>
/// List of module names this module depends upon
/// </summary>
public IReadOnlyList<string> Dependencies => new[]
{
"Tenancy"
};
/// <summary>
/// Integration event contracts published by this module
/// </summary>
public IReadOnlyList<string> PublishedEvents => new[]
{
"BitzOrcas.Modules.LegalKnowledge.Contracts.Events.KnowledgeDocumentPublishedIntegrationEvent"
};
/// <summary>
/// External integration event contracts subscribed to by this module
/// </summary>
public IReadOnlyList<string> SubscribedEvents => new[]
{
"BitzOrcas.Modules.Legal.Contracts.Events.MatterIntakeCreatedIntegrationEvent"
};
/// <summary>
/// Publicly exposed namespaces (other modules are permitted to reference only these)
/// </summary>
public IReadOnlyList<string> PublicContractNamespaces => new[]
{
"BitzOrcas.Modules.LegalKnowledge.Contracts",
"BitzOrcas.Modules.LegalKnowledge.Contracts.Dtos",
"BitzOrcas.Modules.LegalKnowledge.Contracts.Events"
};
/// <summary>
/// Fine-grained RBAC action codes owned by this module
/// </summary>
public IReadOnlyList<string> OwnedPermissions => new[]
{
"legal.knowledge.view",
"legal.knowledge.create",
"legal.knowledge.publish",
"legal.knowledge.delete"
};
/// <summary>
/// Feature capability toggles owned by this module
/// </summary>
public IReadOnlyList<string> OwnedFeatures => new[]
{
"legal.knowledge.fulltext-search"
};
/// <summary>
/// Registers non-mechanical module-specific policy services
/// </summary>
/// <param name="services">DI service collection.</param>
/// <remarks>
/// Repositories and vertical slice handlers are registered automatically at compile time via Roslyn generators.
/// This method is reserved for special adapters or local cache policies.
/// </remarks>
public void ConfigureServices(IServiceCollection services)
{
// Register custom adapters (e.g. vector search clients or localized caches) as needed
}
}

Step 4: Automate Boundary Defense with ArchUnit Tests

Add architectural assertions in tests/BitzOrcas.Architecture.Tests/ModuleBoundaryTests.cs: Ensure that no external modules (e.g. Legal or Billing) reference internal Domain or implementation assemblies:

tests/BitzOrcas.Architecture.Tests/ModuleBoundaryTests.cs
using ArchUnitNET.Domain;
using ArchUnitNET.Fluent;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnit;
using Xunit;
using static ArchUnitNET.Fluent.ArchRuleDefinition;
namespace BitzOrcas.Architecture.Tests;
/// <summary>
/// Architecture boundary automated guardrail tests
/// </summary>
public sealed class ModuleBoundaryTests
{
private static readonly Architecture SolutionArchitecture =
new ArchLoader().LoadAssemblies(
typeof(LegalKnowledge.LegalKnowledgeModule).Assembly,
typeof(LegalKnowledge.Domain.KnowledgeDocument).Assembly,
typeof(LegalKnowledge.Contracts.Dtos.KnowledgeDocumentSummaryDto).Assembly).Build();
[Fact]
public void ExternalModules_MustNotDependOn_LegalKnowledgeInternalDomainOrImplementation()
{
// 1. Define rule: forbid external modules from referencing LegalKnowledge Domain
IArchRule rule = Types().That()
.ResideInNamespace("BitzOrcas.Modules..")
.And()
.DoNotResideInNamespace("BitzOrcas.Modules.LegalKnowledge..")
.ShouldNot()
.DependOnAny(Types().That().ResideInNamespace("BitzOrcas.Modules.LegalKnowledge.Domain.."));
// 2. Perform static analysis on loaded assemblies
rule.Check(SolutionArchitecture);
}
}

Summary

Adhering to the three-tier physical project architecture yields lasting engineering certainty:

  • Physical Project Isolation: .Contracts, .Domain, and host implementation projects enforce boundary integrity at the compiler level;
  • Self-Describing Governance: IAppModule automates permission provisioning, event bus routing, and dependency tracking;
  • Continuous Boundary Defense: ArchUnit tests serve as CI quality gates, guaranteeing architecture principles remain uncompromised over time.

100%

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