Skip to content

DDD Tactical Design Overview

This document provides the complete picture of decomposing domain complexity into clear building blocks and mapping each building block to Functorium framework types.

“Should this logic go in an Entity or a Service?” “Where should the Aggregate boundary be set?” “Should validation failure be thrown as an exception or returned as a result type?”

DDD tactical design is a building block system that provides consistent answers to these questions. This document draws the complete map of those building blocks and maps how the Functorium framework implements each building block as types.

Through this document, you will learn:

  1. The complete structure of DDD tactical design building blocks - Roles and relationships of Value Object, Entity, Aggregate, Domain Event, etc.
  2. Type mapping with the Functorium framework - Functorium types and namespaces corresponding to each building block
  3. Layer architecture and building block placement rules - Responsibilities and dependency direction for Domain, Application, and Adapter layers
  4. Modules and project structure - Layer (horizontal) x Module (vertical) dual-axis placement strategy
  5. Ubiquitous language and naming guide - Central index of naming patterns for all building blocks

A basic understanding of the following concepts is required to understand this document:

  • DDD strategic design concepts (Bounded Context, Ubiquitous Language)
  • Basic C# syntax (classes, interfaces, generics)
  • Layer structure from the Project Structure Guide

The core of DDD tactical design is “decomposing domain complexity into clear building blocks and consistently maintaining the responsibilities and placement of each building block.” Functorium enforces these building blocks through the type system, ensuring design decisions are directly reflected in the code.

// Value Object creation
var email = Email.Create("user@example.com");
// Entity/Aggregate creation
var order = Order.Create(productId, quantity, unitPrice, shippingAddress);
// Domain event publishing
order.AddDomainEvent(new CreatedEvent(order.Id, productId, quantity, totalAmount));
// Specification composition
var spec = priceRange & !lowStock;
// Domain Service usage (within Usecase)
private readonly OrderCreditCheckService _creditCheckService = new();
  1. Define Value Object: Inherit SimpleValueObject<T>, implement Create() + Validate()
  2. Define Entity/Aggregate: Inherit AggregateRoot<TId>, apply [GenerateEntityId] attribute
  3. Define Domain Event: Inherit DomainEvent as a nested sealed record within the Aggregate
  4. Define Specification: Inherit ExpressionSpecification<T>, implement ToExpression()
  5. Define Domain Service: Implement IDomainService marker interface, write as pure function (default) or with Repository usage (Evans Ch.9)
  6. Implement Usecase: Inherit ICommandUsecase<T,R> / IQueryUsecase<T,R>, orchestrate with FinT<IO, T> LINQ chain
ConceptDescriptionFunctorium Type
Value ObjectImmutable, value equality, self-validationSimpleValueObject<T>, ValueObject
Entity / AggregateID equality, consistency boundaryEntity<TId>, AggregateRoot<TId>
Domain EventPast tense, immutable, inter-Aggregate communicationIDomainEvent, DomainEvent
Domain ServiceCross-Aggregate domain logicIDomainService
SpecificationBusiness rule encapsulation, compositionSpecification<T>, ExpressionSpecification<T>
Error HandlingRailway Oriented ProgrammingFin<T>, Validation<Error, T>
Layer StructureDomain -> Application -> AdapterDependency rule: inner layers cannot reference outer layers

The essential complexity of software comes from the domain. DDD tactical design manages this complexity by decomposing it into clear building blocks. Each building block has clear roles and responsibilities, providing developers with consistent answers to the question “Where should this code be placed?”

DDD emphasizes that domain experts and developers should use the same language. When domain terms like Email, Order, and Product are directly expressed as types in code, the code itself becomes the domain model.

Business rules like “Is the email format valid?”, “Is stock sufficient?”, and “Is the order status transition valid?” are placed in specific building blocks (Value Object, Entity, Aggregate), making the location and responsibility of each rule clear.

Without tactical design, business logic is scattered throughout the service layer. Email format validation is performed differently in the controller, service, and repository, making it impossible to answer the question “Where is this rule managed?”

When tactical design is applied, each rule is placed in a clear building block. Email format validation belongs to the Email Value Object, stock shortage validation to the Inventory Aggregate, and order creation orchestration to the Usecase, so the code structure alone reveals where responsibilities lie.

We have examined why DDD tactical design is needed. In the next section, we will explore what each building block is and which types implement them in the Functorium framework.

DDD Tactical Design Building Blocks (WHAT)

Section titled “DDD Tactical Design Building Blocks (WHAT)”

Roles and Relationships of Each Building Block

Section titled “Roles and Relationships of Each Building Block”
Building BlockRoleCharacteristics
Value ObjectValue representation of domain conceptsImmutable, value equality, self-validation
EntityDomain object with an identifierID equality, mutable, lifecycle
AggregateObject group with consistency boundaryTransaction unit, invariant protection
Domain EventImportant occurrence in the domainPast tense, immutable, inter-Aggregate communication
Domain ServiceCross-Aggregate domain logic (pure or with Repository)Stateless, IDomainService marker
FactoryAggregate creation/restorationStatic Create(), CreateFromValidated() methods
RepositoryAggregate persistenceStore/retrieve per Aggregate unit
Application ServiceUsecase orchestrationCommand/Query, delegates to domain objects

The following table shows the complete mapping between DDD building blocks and Functorium framework types. When implementing a new building block, refer to this table for the corresponding type and namespace.

DDD Building BlockFunctorium TypeLocation
Value ObjectSimpleValueObject<T>, ValueObject, ComparableSimpleValueObject<T>Functorium.Domains.ValueObjects
EntityEntity<TId>Functorium.Domains.Entities
Aggregate RootAggregateRoot<TId>Functorium.Domains.Entities
Entity IDIEntityId<T> + [GenerateEntityId]Functorium.Domains.Entities
Domain EventIDomainEvent, DomainEventFunctorium.Domains.Events
Domain ServiceIDomainServiceFunctorium.Domains.Services
SpecificationSpecification<T>Functorium.Domains.Specifications
Domain ErrorDomainError, DomainErrorTypeFunctorium.Domains.Errors
CommandICommandRequest<T>, ICommandUsecase<T,R>Functorium.Applications.Usecases
QueryIQueryRequest<T>, IQueryUsecase<T,R>Functorium.Applications.Usecases
Event HandlerIDomainEventHandler<T>Functorium.Applications.Events
Application ErrorApplicationError, ApplicationErrorTypeFunctorium.Applications.Errors
PortIObservablePortFunctorium.Abstractions.Observabilities
RepositoryIRepository<TAggregate, TId>Functorium.Domains.Repositories
Adapter[GenerateObservablePort]Adapter layer project
Adapter ErrorAdapterError, AdapterErrorTypeFunctorium.Adapters.Errors
ValidationValidationRules<T>, TypedValidation<T,V>Functorium.Domains.ValueObjects.Validations
Result TypeFin<T>, Validation<Error, T>, FinResponse<T>LanguageExt / Functorium

We have confirmed the roles of building blocks and their Functorium type mappings. In the next section, we will examine how Functorium combines DDD and functional programming, and its design philosophy.

Functorium combines Domain-Driven Design (DDD) tactical patterns with functional programming. The following table shows how concepts from the two paradigms are unified in Functorium.

ConceptDDDFunctional ProgrammingFunctorium
Value ObjectImmutable object, value-based equalityImmutable data structureValueObject, SimpleValueObject<T>
ValidationSelf-validation objectType-safe validationValidationRules<T>, TypedValidation<T,V>
Error HandlingException vs ResultRailway Oriented ProgrammingFin<T>, Validation<Error, T>
  1. Type Safety: Prevent errors at compile time
  2. Immutability: All value objects cannot be changed after creation
  3. Self-validation: Objects in invalid states cannot be created
  4. Explicit Error Handling: Use result types instead of exceptions

A Value Object is an immutable object whose equality is determined by its property values.

// Value Object example: Email (see Quick Start example for full implementation)
public sealed class Email : SimpleValueObject<string>
{
private Email(string value) : base(value) { }
public static Fin<Email> Create(string? value) =>
CreateFromValidation(Validate(value), v => new Email(v));
public static Validation<Error, string> Validate(string? value) =>
ValidationRules<Email>.NotEmpty(value ?? "")
.ThenMatches(EmailPattern)
.ThenMaxLength(254);
}

Characteristics of Value Objects:

CharacteristicsDescription
ImmutabilityCannot be changed after creation
Value-based equalityEquality determined by property values
Self-validationValidates at creation time
Domain logic encapsulationIncludes related operations

An Entity is a domain object with a unique identifier (ID). Entities with the same ID are considered identical.

// Entity example: Order (receives validated VOs to create Aggregate)
[GenerateEntityId] // Auto-generates OrderId
public sealed class Order : AggregateRoot<OrderId>
{
public ProductId ProductId { get; private set; }
public Quantity Quantity { get; private set; }
public Money UnitPrice { get; private set; }
public Money TotalAmount { get; private set; }
private Order(OrderId id, ProductId productId, Quantity quantity,
Money unitPrice, Money totalAmount) : base(id) { /* ... */ }
// Create: Receives validated VOs to create new Aggregate
public static Order Create(
ProductId productId, Quantity quantity,
Money unitPrice, ShippingAddress shippingAddress)
{
var totalAmount = unitPrice.Multiply(quantity);
var order = new Order(OrderId.New(), productId, quantity, unitPrice, totalAmount);
order.AddDomainEvent(new CreatedEvent(order.Id, productId, quantity, totalAmount));
return order;
}
}

Entity vs Value Object:

AspectEntityValue Object
IdentifierID-based equalityValue-based equality
MutabilityMutableImmutable
LifecycleLong-term (Repository)Short-term (ephemeral)
ExampleOrder, User, ProductMoney, Email, Address

Value objects always exist only in a valid state:

// Invalid email cannot be created
var result = Email.Create("invalid"); // Fin<Email> - failure
var result = Email.Create("user@example.com"); // Fin<Email> - success

Error Handling Strategy (Railway Oriented Programming)

Section titled “Error Handling Strategy (Railway Oriented Programming)”

Functorium uses result types instead of exceptions:

Input -> [Validation1] -> [Validation2] -> [Validation3] -> Success
↓ ↓ ↓
Failure Failure Failure

Two Result Types:

TypePurposeFeatures
Fin<T>Final resultSuccess or single error
Validation<Error, T>Validation resultSuccess or multiple errors
IValueObject (interface)
AbstractValueObject (abstract class)
├── GetEqualityComponents() - equality components
├── Equals() / GetHashCode() - value-based equality
└── == / != operators
└── ValueObject
├── CreateFromValidation<TValueObject, TValue>() helper
├── SimpleValueObject<T>
│ ├── protected T Value
│ ├── CreateFromValidation<TValueObject>() helper
│ └── explicit operator T
└── ComparableValueObject
├── GetComparableEqualityComponents()
├── IComparable<ComparableValueObject>
├── < / <= / > / >= operators
└── ComparableSimpleValueObject<T>
├── protected T Value
├── CreateFromValidation<TValueObject>() helper
└── explicit operator T
IEntity<TId> (interface)
├── TId Id - Entity identifier
├── CreateMethodName constant
└── CreateFromValidatedMethodName constant
└── Entity<TId> (abstract class)
├── Id property (protected init)
├── Equals() / GetHashCode() - ID-based equality
├── == / != operators
├── CreateFromValidation<TEntity, TValue>() helper
└── GetUnproxiedType() - ORM proxy support
└── AggregateRoot<TId> : IDomainEventDrain
├── DomainEvents (read-only, IHasDomainEvents)
├── AddDomainEvent() (protected)
└── ClearDomainEvents() (IDomainEventDrain)
IEntityId<T> : IParsable<T> (interface) - Ulid-based
├── Ulid Value
├── static T New()
├── static T Create(Ulid)
└── static T Create(string)
IDomainEvent : INotification (interface)
├── DateTimeOffset OccurredAt
├── Ulid EventId
├── string? CorrelationId
└── string? CausationId
└── DomainEvent (abstract record)
├── Default constructor: OccurredAt, EventId auto-set
└── CorrelationId, CausationId optionally specified
IHasDomainEvents (read-only event query)
└── IDomainEventDrain (internal, event cleanup)
Error (LanguageExt)
├── DomainError
│ └── DomainErrorType (Presence, Length, Format, DateTime, Numeric, Range, Existence, Transition, Custom)
├── ApplicationError
│ └── ApplicationErrorType (Common, Authorization, Validation, Business Rules, Custom)
└── AdapterError
└── AdapterErrorType (Common, Pipeline, External Service, Data, Custom)
Specification<T> (abstract class)
├── abstract bool IsSatisfiedBy(T entity)
├── And() / Or() / Not() composition methods
├── & / | / ! operator overloads
├── AndSpecification<T> (internal sealed)
├── OrSpecification<T> (internal sealed)
├── NotSpecification<T> (internal sealed)
├── ExpressionSpecification<T> (public abstract)
└── AllSpecification<T> (internal sealed)
+-------------------+ +-------------------+
| ValueObject | | Validation |
| |◄────────| ValidationRules |
+-------------------+ +-------------------+
│ │
│ │
▼ ▼
+-------------------+ +-------------------+
| Fin<T> / | | DomainError |
| Validation<E,T> |◄────────| |
+-------------------+ +-------------------+

Now that we understand the design philosophy and core concepts, let’s explore which layer each building block is placed in within an actual project.

Layer Architecture and Building Block Placement

Section titled “Layer Architecture and Building Block Placement”

Handles core business logic of the domain. No external dependencies.

  • Building blocks placed: Value Object, Entity, Aggregate Root, Domain Event, Domain Error, Domain Service, Repository Interface
  • Dependencies: None (innermost layer)

Orchestrates use cases. Delegates work to domain objects.

  • Building blocks placed: Command/Query (Use Case), Event Handler, Application Error, Port Interface
  • Dependencies: Depends only on Domain Layer

Handles communication with external systems.

  • Building blocks placed: Adapter implementations, Pipeline (auto-generated), Adapter Error
  • Dependencies: Depends on Domain Layer and Application Layer
Adapter Layer → Application Layer → Domain Layer
(outer) (middle) (inner)

Inner layers never reference outer layers. When the Application Layer needs Adapter functionality, it defines a Port (interface), and the Adapter Layer implements it.

Eric Evans defines Module as a grouping unit based on cohesion of domain concepts. A Module is not a package or namespace but a semantic boundary.

PrincipleDescription
High cohesionElements within the same Module express a single domain concept
Low couplingDependencies between Modules are minimized; communication via Port/Interface when needed
CommunicationModule names reflect ubiquitous language, conveying domain boundaries through code structure alone

Functorium places code along dual axes of Layer (horizontal axis) and Module (vertical axis).

  • Layer — .csproj unit. Separates technical concerns (Domain, Application, Adapter)
  • Module — Folder/namespace unit. Maintains cohesion of domain concepts (Products, Orders, etc.)
│ Products │ Inventories │ Orders │ Customers │ SharedModels │
──────────────┼───────────┼─────────────┼─────────┼───────────┼──────────────┤
Domain │ Aggregate │ Aggregate │Aggregate│ Aggregate │ VO, Entity, │
(.csproj) │ VO, Spec │ Spec │ VO │ VO, Spec │ Event │
│ Port │ Port │ Port │ Port │ │
──────────────┼───────────┼─────────────┼─────────┼───────────┼──────────────┤
Application │ Command │ Command │ Command │ Command │ │
(.csproj) │ Query │ Query │ Query │ Query │ │
│ EventHdlr │ EventHdlr │EventHdlr│ EventHdlr │ │
──────────────┼───────────┼─────────────┼─────────┼───────────┼──────────────┤
Adapter │ Endpoint │ Endpoint │Endpoint │ Endpoint │ │
(.csproj ×3) │ Repo │ Repo │ Repo │ Repo │ │
│ QueryAdpt │ QueryAdpt │ │ │ │
──────────────┴───────────┴─────────────┴─────────┴───────────┴──────────────┘

Mapping Rules:

AxisUnitSeparation CriteriaExample
Layer (horizontal).csprojTechnical concerns, dependency directionLayeredArch.Domain, LayeredArch.Application
Module (vertical)Folder/namespaceDomain concept cohesionAggregateRoots/Products/, Usecases/Products/

This is the actual module configuration of the SingleHost project.

ModuleDomainApplicationAdapter
ProductsAggregateRoots/Products/ (Aggregate, Ports, Specs, VOs)Usecases/Products/ (Commands, Queries, Dtos, Ports)Endpoints, Repository, Query
InventoriesAggregateRoots/Inventories/ (Aggregate, Ports, Specs)Usecases/Inventories/ (Commands, Queries, Dtos, Ports)Endpoints, Repository, Query
OrdersAggregateRoots/Orders/ (Aggregate, Ports, VOs)Usecases/Orders/ (Commands, Queries)Endpoints, Repository
CustomersAggregateRoots/Customers/ (Aggregate, Ports, Specs, VOs)Usecases/Customers/ (Commands, Queries)Endpoints, Repository
SharedModelsSharedModels/ (shared VO, Entity, Event)

Pattern: Each Module is a vertical slice that cuts through all layers from Domain → Application → Adapter. The folder name is the Module name, and the Module name is the ubiquitous language.

The domain layer of the host project follows the following folder structure.

Reference example (01-SingleHost LayeredArch.Domain/):

LayeredArch.Domain/
├── AggregateRoots/
│ ├── Customers/
│ │ ├── Customer.cs
│ │ ├── ICustomerRepository.cs
│ │ ├── Specifications/
│ │ │ └── CustomerEmailSpec.cs
│ │ └── ValueObjects/
│ │ ├── CustomerName.cs
│ │ └── Email.cs
│ ├── Inventories/
│ │ ├── Inventory.cs
│ │ ├── IInventoryRepository.cs
│ │ └── Specifications/
│ │ └── InventoryLowStockSpec.cs
│ ├── Orders/
│ │ ├── Order.cs
│ │ ├── IOrderRepository.cs
│ │ └── ValueObjects/
│ │ └── ShippingAddress.cs
│ └── Products/
│ ├── Product.cs
│ ├── IProductRepository.cs
│ ├── Specifications/
│ │ ├── ProductNameSpec.cs
│ │ ├── ProductNameUniqueSpec.cs
│ │ └── ProductPriceRangeSpec.cs
│ └── ValueObjects/
│ ├── ProductDescription.cs
│ └── ProductName.cs
├── SharedModels/
│ ├── Entities/
│ │ ├── Tag.cs
│ │ └── ValueObjects/
│ │ └── TagName.cs
│ ├── Services/
│ │ └── OrderCreditCheckService.cs
│ └── ValueObjects/
│ ├── Money.cs
│ └── Quantity.cs
├── DOMAIN-GLOSSARY.md
├── Using.cs
└── AssemblyReference.cs

Structure Summary:

  • AggregateRoots/{Aggregate}/ — Aggregate root, repository interface, sub-folders Specifications/ and ValueObjects/
  • SharedModels/Entities/, Services/, ValueObjects/ shared across multiple Aggregates
  • Root — DOMAIN-GLOSSARY.md, Using.cs, AssemblyReference.cs

Placement within Module (default)

  • Types specific to a particular Aggregate → Inside that Aggregate’s folder
  • Example: ProductNameAggregateRoots/Products/ValueObjects/

Criteria for moving to SharedModels

  • Types shared by 2 or more Aggregates → SharedModels/
  • Example: Money, QuantitySharedModels/ValueObjects/

Criteria for moving to project root

  • Cross-Aggregate Port → Domain/Ports/ (e.g., IProductCatalog — for Product validation from Order)
  • Domain Service → Domain/Services/ (e.g., OrderCreditCheckService — cross-Aggregate pure logic)

Initially place as Aggregate-specific, and move to SharedModels when sharing becomes necessary. For detailed criteria on this rule, see 01-project-structure.md FAQ §3.

This is the 3-stage evolution path of module structure as a service grows.

StepStructureDescription
Stage 1Single AggregateOne Aggregate is one Module. SingleHost initial Product structure
Stage 2Multi-Aggregate same serviceMultiple Aggregates separated into folders but placed within the same service (process). SingleHost current structure
Stage 3Separate Bounded ContextModules separated into independent services (.sln). Context Map patterns required

Stage 2 → Stage 3 separation criteria:

CriteriaKeep same serviceSeparate into different service
Deployment cycleSameIndependent deployment per Module needed
Transaction boundaryAggregates can share the same DBIndependent DB/schema needed
Team ownershipSame teamDifferent teams develop independently
Ubiquitous languageNo term conflictsSame terms with different meanings
Data storageHomogeneous (e.g., all PostgreSQL)Heterogeneous (e.g., SQL + NoSQL)

Note: Stage 3 Bounded Context separation patterns (Context Map, ACL, etc.) are covered in §8 Bounded Context Boundary Definition below.

Detailed naming rules for each building block are documented in individual guides. This section serves as a central index where all building block naming patterns can be referenced in one place.

The following table is a central index that consolidates naming rules for all building blocks in one place. When adding new types, refer to this table to assign consistent names.

Building BlockNaming PatternExampleDetail Reference
Value Object{Concept}ProductName, Email05a-value-objects.md
Entity{EntityName}Tag06b-entity-aggregate-core.md
Aggregate Root{Aggregate}Product, Order06b-entity-aggregate-core.md
Entity ID{Aggregate}Id + [GenerateEntityId]ProductId, OrderId06b-entity-aggregate-core.md
Domain Event{Aggregate}.{PastTense}Event (nested record)Product.CreatedEvent07-domain-events.md
Domain ErrorDomainError.For<{Type}>()DomainError.For<Email>()08b-error-system-domain-app.md
Domain Service{DomainConcept}Service : IDomainServiceOrderCreditCheckService09-domain-services.md
Specification{Aggregate}{Concept}SpecProductNameUniqueSpec10-specifications.md
Command{Verb}{Aggregate}Command (nested Request/Response/Usecase)CreateProductCommand11-usecases-and-cqrs.md
Query{Get/Search}{Description}Query (nested Request/Response/Usecase)SearchProductsQuery11-usecases-and-cqrs.md
Event HandlerOn{DomainEvent}OnProductCreated01-project-structure.md
Repository InterfaceI{Aggregate}RepositoryIProductRepository12-ports.md
Repository Impl{Technology}{Aggregate}RepositoryEfCoreProductRepository13-adapters.md
Query Adapter InterfaceI{Aggregate}QueryIProductQuery12-ports.md
Query Adapter Impl{Technology}{Aggregate}QueryDapperProductQuery13-adapters.md
Cross-Aggregate PortI{Concept}IProductCatalog12-ports.md
Endpoint{Verb}{Aggregate}EndpointCreateProductEndpoint01-project-structure.md
Persistence Model{Aggregate}ModelProductModel13-adapters.md
Mapper{Aggregate}MapperProductMapper13-adapters.md
Module (folder)Plural noun (ubiquitous language)Products/, Orders/§6

Maintaining a glossary shared between domain experts and developers prevents discrepancies between code naming and business terminology.

Domain TermDefinitionCode TypeNotes
ProductIndividual item in the sales catalogProduct (Aggregate)
InventoryAvailable quantity of a productInventory (Aggregate)1:1 with Product
OrderCustomer’s purchase requestOrder (Aggregate)
AmountCurrency + numeric combinationMoney (Value Object)SharedModels
QuantityInteger value of 0 or greaterQuantity (Value Object)SharedModels

Usage: Create a per-project glossary in the above format and share with domain experts. When terms change, code type names should be updated accordingly.

  • The glossary is maintained through iterative agreement between domain experts and developers.
  • Using names in code that differ from domain terms increases communication costs. When term conflicts are discovered, update the glossary immediately and rename the code.
  • When adding new building blocks, refer to the naming pattern table above to assign consistent names.

The current SingleHost project operates multiple Modules (Products, Orders, etc.) within a single Bounded Context. This section defines Context Map patterns to apply when the service grows and is separated into multiple Bounded Contexts, and identifies precedent patterns that already exist in the current code.

PatternDescriptionFunctorium Mapping
Shared KernelSubset of domain model shared by two BCsSharedModels/ folder (Money, Quantity)
Customer-SupplierUpstream BC provides API to downstream BCNot implemented (future inter-service REST API)
Anti-Corruption Layer (ACL)Translation layer to prevent external model contaminationIProductCatalog Port + EF Core Mapper
Open Host ServicePublic API provided via standard protocolREST Endpoints
Published LanguageShared language between BCs (events/schemas)Domain Events (future Integration Event)
ConformistDownstream accepts upstream model as-isNot implemented
Separate WaysIndependent operation without BC integrationNot implemented

Recognizing Precedent Patterns in SingleHost

Section titled “Recognizing Precedent Patterns in SingleHost”

Existing code already contains single-service precedent implementations of Context Map patterns. These patterns become integration points between BCs when services are separated.

Shared Kernel → SharedModels/ValueObjects/

Value Objects shared by multiple Modules such as Money and Quantity are placed in the SharedModels/ folder. When separating services, a decision is needed to either extract them as NuGet packages or duplicate them in each BC.

ACL (mini) → IProductCatalog Port + Adapter

When the Order Module queries Product data, it accesses through the IProductCatalog Port. Currently it’s an EF Core implementation within the same process, but when services are separated, it will be replaced with a remote API call + response translation layer (ACL).

Domain Events as Published Language

Currently Domain Events are published via an in-process Mediator. When services are separated, they will transition to Integration Events through a message broker (RabbitMQ, Kafka, etc.), at which point separation of Domain Events and Integration Events becomes necessary.

This is the conceptual project structure when separating into multiple Bounded Contexts.

Services/
├── ProductCatalog/ ← BC 1 (same existing 3-Layer structure)
│ ├── ProductCatalog.Domain/
│ ├── ProductCatalog.Application/
│ └── ProductCatalog.Adapters.*/
├── OrderManagement/ ← BC 2
│ ├── OrderManagement.Domain/
│ ├── OrderManagement.Application/
│ └── OrderManagement.Adapters.*/
SharedModels/ ← Shared NuGet package
IntegrationEvents/ ← Published Language (shared event schema between BCs)

Each BC maintains the same 3-Layer structure (Domain → Application → Adapter) from §5. Only inter-BC communication is replaced with Integration Events or REST APIs instead of Cross-Aggregate Ports.

The Multi-Aggregate Expansion Guide in §6 presented Stage 3 separation criteria (WHEN). The Context Map patterns in this section guide how (HOW) to implement once separation is decided.

  • WHEN: Deployment cycle, transaction boundary, team ownership, ubiquitous language conflicts, data storage heterogeneity → §6 criteria table
  • HOW: Shared Kernel, ACL, Published Language, Open Host Service → Context Map patterns in this section
using Functorium.Domains.ValueObjects;
using Functorium.Domains.ValueObjects.Validations.Typed;
using System.Text.RegularExpressions;
public sealed class Email : SimpleValueObject<string>
{
private static readonly Regex EmailPattern = new(
@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
RegexOptions.Compiled);
private const int MaxLength = 254;
// private constructor - prevents external creation
private Email(string value) : base(value) { }
// Factory method
public static Fin<Email> Create(string? value) =>
CreateFromValidation(Validate(value), v => new Email(v));
// Validation method (returns primitive type)
public static Validation<Error, string> Validate(string? value) =>
ValidationRules<Email>.NotEmpty(value ?? "")
.ThenNormalize(v => v.ToLowerInvariant())
.ThenMatches(EmailPattern)
.ThenMaxLength(MaxLength);
// Implicit conversion (optional)
public static implicit operator string(Email email) => email.Value;
}
// Success
var email = Email.Create("User@Example.COM");
email.IfSucc(e => Console.WriteLine(e)); // user@example.com
// Failure
var invalid = Email.Create("invalid-email");
invalid.IfFail(e => Console.WriteLine(e.Code)); // DomainErrors.Email.InvalidFormat
using Functorium.Testing.Assertions.Errors;
using static Functorium.Domains.Errors.DomainErrorType;
[Fact]
public void Create_ShouldFail_WhenEmailIsEmpty()
{
// Arrange
var emptyEmail = "";
// Act
var result = Email.Create(emptyEmail);
// Assert
result.ShouldBeDomainError<Email, Email>(new Empty());
}
[Fact]
public void Create_ShouldSucceed_WhenEmailIsValid()
{
// Arrange
var validEmail = "user@example.com";
// Act
var result = Email.Create(validEmail);
// Assert
result.IsSucc.ShouldBeTrue();
}
DocumentDescriptionKey Content
05a-value-objects.mdValue Object implementationBase classes, validation system, implementation patterns, practical examples
05b-value-objects-validation.mdValue Object validation and enumsEnum implementation, Application validation, FAQ
06a-aggregate-design.mdAggregate designDesign principles, boundary setting, anti-patterns
06b-entity-aggregate-core.mdEntity/Aggregate core patternsClass hierarchy, ID system, creation patterns, domain events
06c-entity-aggregate-advanced.mdEntity/Aggregate advanced patternsCross-Aggregate relationships, auxiliary interfaces, practical examples
07-domain-events.mdDomain eventsEvent definition, publishing, handler implementation
08a-error-system.mdError system: basics and namingError handling principles, Fin patterns, naming rules
08b-error-system-domain-app.mdError system: Domain/Application errorsDomain/Application/Event error definition and testing
08c-error-system-adapter-testing.mdError system: Adapter errors and testingAdapter errors, Custom errors, testing best practices, checklists
11-usecases-and-cqrs.mdUsecase implementationCQRS pattern, Apply merging
12-ports.mdPort architecturePort definition, IObservablePort hierarchy
13-adapters.mdAdapter implementationRepository, External API, Messaging, Query
14a-adapter-pipeline-di.mdAdapter integrationPipeline, DI, Options
14b-adapter-testing.mdAdapter testingUnit tests, E2E Walkthrough
09-domain-services.mdDomain servicesIDomainService, cross-Aggregate logic, Usecase integration
10-specifications.mdSpecification patternBusiness rule encapsulation, And/Or/Not composition, Repository integration
15a-unit-testing.mdUnit TestingTest rules, naming, checklist
16-testing-library.mdTesting LibraryLog/architecture/source generator/Job testing

You can see actual implementations in the LayeredArch example project:

ConceptExample File
Value ObjectTests.Hosts/01-SingleHost/LayeredArch.Domain/ValueObjects/
EntityTests.Hosts/01-SingleHost/LayeredArch.Domain/Entities/Product.cs
RepositoryTests.Hosts/01-SingleHost/Src/LayeredArch.Domain/AggregateRoots/Products/Ports/IProductRepository.cs
Repository (common)Src/Functorium/Domains/Repositories/IRepository.cs
UsecaseTests.Hosts/01-SingleHost/LayeredArch.Application/Usecases/Products/
Domain ServiceTests.Hosts/01-SingleHost/Src/LayeredArch.Domain/Services/OrderCreditCheckService.cs
SpecificationTests.Hosts/01-SingleHost/Src/LayeredArch.Domain/AggregateRoots/Products/Specifications/
AdapterTests.Hosts/01-SingleHost/LayeredArch.Adapters.Persistence/Repositories/

Cause: The Validate() method may not handle null or empty strings, or the regex pattern may be incorrect.

Solution: Check null handling in the Validate() method and use the ValidationRules<T>.NotEmpty(value ?? "") pattern. Verify regex patterns with separate unit tests.

Entity IDs Are Not Compared (Equality Failure)

Section titled “Entity IDs Are Not Compared (Equality Failure)”

Cause: An ID type may have been defined directly without the [GenerateEntityId] attribute, or IEntityId<T> may not have been implemented.

Solution: Always use the [GenerateEntityId] Source Generator for Entity IDs. The Source Generator automatically generates Equals(), GetHashCode(), ==, and != operators.

Cause: This occurs when role distinctions between building blocks are not clear.

Solution: Follow these decision criteria:

  1. Within a single Aggregate -> Entity method or Value Object
  2. References multiple Aggregates + no I/O -> Domain Service
  3. Requires I/O (Repository, external API) -> Orchestrate in Usecase
  4. Side effects after state change -> Domain Event + Event Handler

Q1. What is the criteria for choosing between Value Object and Entity?

Section titled “Q1. What is the criteria for choosing between Value Object and Entity?”

The key factor is whether an identifier (ID) is needed. If equality is determined by the value itself like Money and Email, it is a Value Object. If it needs to be tracked by a unique ID like Order and Product, it is an Entity. Generally, Value Objects should be more numerous, with Entities being the minority.

The scope that must guarantee consistency within a single transaction is the Aggregate boundary. Keep Aggregates small, and use only IDs for cross-Aggregate references. See 06a-aggregate-design.md for detailed design principles.

Q3. What are the criteria for types that should be placed in SharedModels?

Section titled “Q3. What are the criteria for types that should be placed in SharedModels?”

Value Objects or Entities shared by 2 or more Aggregates are the target. Initially, place them within a specific Aggregate, and move to SharedModels/ when sharing is actually needed.

Q4. When do you use Fin<T> vs Validation<Error, T>?

Section titled “Q4. When do you use Fin<T> vs Validation<Error, T>?”

Fin<T> is for final results (success or single error), while Validation<Error, T> is for validation results (accumulating multiple errors). Value Object’s Create() returns Fin<T>, while Validate() returns Validation<Error, T>.

Q5. When should you split into multiple Bounded Contexts?

Section titled “Q5. When should you split into multiple Bounded Contexts?”

Consider splitting when deployment cycles differ, team ownership is separated, the same terms are used with different meanings, or heterogeneous data stores are needed. Apply Context Map patterns (Shared Kernel, ACL, Published Language, etc.) for the splitting method.