Previous Agile-and-QA-Metrics Security in NET Core Next

Domain-Driven Design (DDD): Entities and Value Objects with Practical Examples

Domain-Driven Design (DDD): A deep dive with entities and value objects

Domain-Driven Design (DDD) is a collaboration-first approach to software that turns complex business knowledge into clear, maintainable models. This article focuses on the two tactical building blocks you use constantly—Entities and Value Objects—while situating them in the broader DDD picture of aggregates, domain services, bounded contexts, and integration patterns. Everything here is presented in plain HTML so you can lift, adapt, and teach with it easily.

Why DDD matters

DDD shines where complexity lives. When business rules evolve, exceptions proliferate, and communication between technical and domain experts frays, DDD realigns the software model with the language and decision-making of the business. The outcomes are clarity, evolvability, and a codebase that expresses intent rather than incidental detail.

  • Focus on the core domain: Invest your best modeling energy where the business differentiates, not in generic plumbing.
  • Ubiquitous language: Build and rigorously use a shared vocabulary across conversations, code, tests, and documentation.
  • Model-code alignment: Keep class names, method names, and module boundaries aligned to domain concepts, not technical artifacts.
  • Bounded context thinking: Accept that terms change meaning across contexts, and protect those meanings within clear boundaries.

Strategic vs. tactical design

Strategic design

  • Bounded contexts: Natural semantic boundaries where a specific model and language apply without ambiguity.
  • Context map: A diagram of how contexts relate (e.g., customer–supplier, conformist, anti-corruption layer, shared kernel).
  • Integration patterns: Choose APIs, events, or translation layers to keep models clean while enabling collaboration.

Tactical design

  • Entities: Objects defined by identity; they change over time but remain the “same” thing.
  • Value objects: Immutable objects defined only by their attributes and meaning; they model measurements, amounts, and concepts.
  • Aggregates: Consistency boundaries with an aggregate root that protects invariants.
  • Repositories: Persistence abstractions that load and store aggregate roots.
  • Domain services: Operations that don’t belong naturally to a single entity or value object.
  • Factories and specifications: Use factories to construct complex objects; use specifications for expressive business rules.
  • Domain events: Signals that something meaningful has happened in the domain, often triggering reactions in-process or across contexts.

Entities

An entity is defined by continuity of identity across state changes. If two objects can have the same attribute values but still be different things, you are dealing with entities. Entities are mutable by design because the domain evolves over time: orders get paid, shipments progress, students change programs.

Characteristics

  • Stable identity: A unique identifier (e.g., Guid/UUID) persists across state transitions and storage.
  • Behavior-rich: Methods enforce business rules and maintain invariants; entities are not mere property bags.
  • Lifecycle: Created, modified, archived; lifecycle transitions carry rules (who can change what and when).
  • Equality by identity: Two entities are the same if their identity is the same, regardless of attribute differences.

C# example: Customer entity

public sealed class Customer
{
    public Guid Id { get; }
    public string FullName { get; private set; }
    public Address Address { get; private set; }
    public Email Email { get; private set; }
    public bool IsActive { get; private set; }

    private Customer(Guid id, string fullName, Address address, Email email)
    {
        Id = id;
        FullName = fullName;
        Address = address;
        Email = email;
        IsActive = true;
    }

    public static Customer Register(string fullName, Address address, Email email)
    {
        if (string.IsNullOrWhiteSpace(fullName))
            throw new ArgumentException("Full name is required.", nameof(fullName));

        return new Customer(Guid.NewGuid(), fullName.Trim(), address, email);
    }

    public void ChangeAddress(Address newAddress)
    {
        if (!IsActive) throw new InvalidOperationException("Cannot change address on inactive customer.");
        Address = newAddress;
    }

    public void Deactivate(string reason)
    {
        if (!IsActive) return;
        // potential domain event: CustomerDeactivated
        IsActive = false;
    }

    public override bool Equals(object obj) => obj is Customer other && Id == other.Id;
    public override int GetHashCode() => Id.GetHashCode();
}

Heuristics to identify entities

  • Continuity matters: If you track the same conceptual thing over time, it’s likely an entity.
  • Conflicts over concurrency: If simultaneous updates can conflict and must be resolved, think entity.
  • External references: If other parts of the system reference it by ID, it is almost certainly an entity.

Value Object in Domain-Driven Design (DDD)

In Domain-Driven Design (DDD), a Value Object is an object that represents a descriptive aspect of the domain and is defined solely by its attribute values, not by a unique identity.

Value objects capture quantities, measurements, and concepts with meaning but no identity. They are immutable and interchangeable when their values match. Value objects make models safer and clearer by encapsulating validation, units, and operations.

Key Characteristics of a Value Object

  • Immutability: Once a Value Object is created, its internal state and attribute values cannot be changed. Any operation that seemingly modifies a Value Object actually returns a new instance with the updated values. This immutability simplifies reasoning about the object's state and makes it safe to share across different parts of the system.
  • No Identity: Unlike Entities, Value Objects do not have a unique identifier (like an ID) that distinguishes one instance from another. Their identity is determined by the combination of their attribute values. If two Value Objects have the same attribute values, they are considered equal.
  • Structural Equality: Equality between two Value Objects is determined by comparing the values of their attributes, not by comparing their memory references.
  • Self-validation: Enforce invariants at construction time (e.g., non-empty email, valid currency code).
  • Explicit meaning: Replace primitive obsession with rich types (e.g., Money instead of decimal, Email instead of string).

Examples of Value Objects

  • Money: An object representing an amount of currency, with attributes like amount and currency. Two Money objects are equal if they represent the same amount in the same currency.
  • Address: An object representing a physical location, with attributes like street, city, state, and zipCode. Two Address objects are equal if all their corresponding attributes have the same values.
  • EmailAddress: An object encapsulating an email string, potentially with built-in validation.

Purpose in DDD

Value Objects help to create a richer and more expressive domain model by encapsulating related data and behavior. They enforce domain invariants and promote immutability, leading to more robust and predictable systems. They also help to reduce the complexity of the domain model by representing concepts that don't require a distinct identity.

C# example: Address value object

public sealed class Address : IEquatable<Address>
{
    public string Line1 { get; }
    public string Line2 { get; }
    public string City { get; }
    public string State { get; }
    public string PostalCode { get; }
    public string CountryCode { get; }

    public Address(string line1, string line2, string city, string state, string postalCode, string countryCode)
    {
        Line1 = Require(line1, nameof(line1));
        Line2 = line2?.Trim();
        City = Require(city, nameof(city));
        State = Require(state, nameof(state));
        PostalCode = Require(postalCode, nameof(postalCode));
        CountryCode = Require(countryCode, nameof(countryCode)).ToUpperInvariant();
    }

    private static string Require(string value, string name)
    {
        if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException($"{name} is required.", name);
        return value.Trim();
    }

    public Address WithPostalCode(string newPostalCode) =>
        new Address(Line1, Line2, City, State, Require(newPostalCode, nameof(newPostalCode)), CountryCode);

    public bool Equals(Address other)
    {
        if (other is null) return false;
        return Line1 == other.Line1 && Line2 == other.Line2 && City == other.City
            && State == other.State && PostalCode == other.PostalCode && CountryCode == other.CountryCode;
    }

    public override bool Equals(object obj) => Equals(obj as Address);
    public override int GetHashCode() => HashCode.Combine(Line1, Line2, City, State, PostalCode, CountryCode);
    public override string ToString() => $"{Line1}, {City}, {State} {PostalCode}, {CountryCode}";
}

C# example: Money value object

public sealed class Money : IEquatable<Money>
{
    public decimal Amount { get; }
    public string Currency { get; } // ISO 4217 code like "USD", "INR", "EUR"

    public Money(decimal amount, string currency)
    {
        if (string.IsNullOrWhiteSpace(currency)) throw new ArgumentException("Currency is required", nameof(currency));
        Currency = currency.Trim().ToUpperInvariant();
        Amount = decimal.Round(amount, 2, MidpointRounding.AwayFromZero);
    }

    public Money Add(Money other)
    {
        EnsureSameCurrency(other);
        return new Money(Amount + other.Amount, Currency);
    }

    public Money Multiply(int factor) => new Money(Amount * factor, Currency);

    private void EnsureSameCurrency(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("Currency mismatch.");
    }

    public bool Equals(Money other) => other is not null && Amount == other.Amount && Currency == other.Currency;
    public override bool Equals(object obj) => Equals(obj as Money);
    public override int GetHashCode() => HashCode.Combine(Amount, Currency);
    public override string ToString() => $"{Currency} {Amount:N2}";
}

C# example: Email value object

public sealed class Email : IEquatable<Email>
{
    public string Value { get; }

    public Email(string value)
    {
        if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Email is required", nameof(value));
        var trimmed = value.Trim();
        if (!trimmed.Contains("@") || trimmed.StartsWith("@") || trimmed.EndsWith("@"))
            throw new ArgumentException("Invalid email format.", nameof(value));
        Value = trimmed.ToLowerInvariant();
    }

    public bool Equals(Email other) => other is not null && Value == other.Value;
    public override bool Equals(object obj) => Equals(obj as Email);
    public override int GetHashCode() => Value.GetHashCode(StringComparison.OrdinalIgnoreCase);
    public override string ToString() => Value;
}

Heuristics to identify value objects

  • No identity needed: If you never need to distinguish two instances with equal values, it’s a value object.
  • Natural immutability: If changing “it” conceptually creates a new thing, it belongs as a value object.
  • Arithmetic or combination: If it supports meaningful operations (e.g., add, multiply, compare), it’s a strong VO candidate.
  • Validation-centric: If rules are about shape/format/range, encapsulate them in a VO to avoid scattered checks.

Aggregates and invariants

Aggregates define consistency boundaries within which invariants are always upheld. The aggregate root is the only entry point for modification; external objects never hold direct references to internal entities. Transactions typically align with aggregate boundaries.

Design rules

  • One root, one gate: Only the root is loaded/saved by repositories; it mediates changes to internal members.
  • Enforce invariants: Keep rules (e.g., total cannot be negative, stock cannot go below zero) consistent at all times.
  • Small and focused: Keep aggregates small to avoid contention and improve performance.
  • Refer by identity: Refer to other aggregates by ID to avoid large object graphs and accidental consistency bleed.

C# example: Order aggregate

public sealed class Order
{
    public Guid Id { get; }
    public Guid CustomerId { get; }
    private readonly List<OrderLine> _lines = new();
    public IReadOnlyCollection<OrderLine> Lines => _lines.AsReadOnly();
    public bool IsConfirmed { get; private set; }

    private Order(Guid id, Guid customerId)
    {
        Id = id;
        CustomerId = customerId;
    }

    public static Order Create(Guid customerId) => new Order(Guid.NewGuid(), customerId);

    public void AddLine(Guid productId, Money unitPrice, int quantity)
    {
        if (IsConfirmed) throw new InvalidOperationException("Cannot modify a confirmed order.");
        if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));

        var existing = _lines.SingleOrDefault(l => l.ProductId == productId && l.UnitPrice.Equals(unitPrice));
        if (existing is not null)
        {
            _lines[_lines.IndexOf(existing)] = existing.Increase(quantity);
        }
        else
        {
            _lines.Add(OrderLine.Create(productId, unitPrice, quantity));
        }

        if (_lines.Count > 100)
            throw new InvalidOperationException("Too many lines on a single order.");
    }

    public Money Total()
    {
        if (_lines.Count == 0) return new Money(0, "INR");
        string currency = _lines.First().UnitPrice.Currency;
        var sum = _lines.Aggregate(0m, (acc, l) => acc + (l.UnitPrice.Amount * l.Quantity));
        return new Money(sum, currency);
    }

    public void Confirm()
    {
        if (IsConfirmed) return;
        if (_lines.Count == 0) throw new InvalidOperationException("Cannot confirm an empty order.");
        IsConfirmed = true;
        // raise domain event: OrderConfirmed
    }
}

public sealed class OrderLine : IEquatable<OrderLine>
{
    public Guid ProductId { get; }
    public Money UnitPrice { get; }
    public int Quantity { get; }

    private OrderLine(Guid productId, Money unitPrice, int quantity)
    {
        ProductId = productId;
        UnitPrice = unitPrice;
        Quantity = quantity;
    }

    public static OrderLine Create(Guid productId, Money unitPrice, int quantity)
    {
        if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
        return new OrderLine(productId, unitPrice, quantity);
    }

    public OrderLine Increase(int by)
    {
        if (by <= 0) throw new ArgumentOutOfRangeException(nameof(by));
        return new OrderLine(ProductId, UnitPrice, Quantity + by);
    }

    public bool Equals(OrderLine other)
    {
        if (other is null) return false;
        return ProductId == other.ProductId && UnitPrice.Equals(other.UnitPrice) && Quantity == other.Quantity;
    }

    public override bool Equals(object obj) => Equals(obj as OrderLine);
    public override int GetHashCode() => HashCode.Combine(ProductId, UnitPrice, Quantity);
}

Notes on persistence

  • Repository per aggregate root: Keep the interface focused on common tasks (GetById, Add, Update, Remove, Save).
  • Optimistic concurrency: Use a version/timestamp to detect conflicting updates.
  • Mapping value objects: In object-relational mappers, use owned types/embeddables for VOs to keep aggregates cohesive.

Domain services

Some operations don’t belong to an entity or value object because they model actions across multiple aggregates or external policies. Domain services hold these behaviors while remaining stateless and pure in terms of inputs/outputs.

When to use

  • Cross-aggregate logic: Price calculation involving catalogs, promotions, and customer tiers.
  • External policy: Tax or exchange rate calculation where data originates outside the domain.
  • Pure domain operations: Validation/specification checks that combine multiple rules.

C# example: Pricing policy

public interface IPricingPolicy
{
    Money PriceFor(Guid productId, Guid customerId, int quantity, string currency);
}

public sealed class DefaultPricingPolicy : IPricingPolicy
{
    public Money PriceFor(Guid productId, Guid customerId, int quantity, string currency)
    {
        // Simplified example: base price 100, volume discount 5% for 10+ items.
        var basePrice = new Money(100m, currency);
        var total = basePrice.Multiply(quantity);
        if (quantity >= 10)
            total = new Money(total.Amount * 0.95m, currency);
        return total;
    }
}

Example domain: Online learning platform

Consider a platform with courses, students, enrollments, and assessments. We’ll sketch bounded contexts and model core entities and value objects.

Bounded contexts

  • Catalog context: Manages courses, modules, and published curricula.
  • Learner context: Manages student profiles, enrollments, progress, and submissions.
  • Billing context: Manages invoices, payments, and refunds.
  • Reporting context: Aggregates data for analytics without polluting core models.

Entities and value objects

  • Student (Entity): Identified by StudentId, holds Email, Name, and status.
  • Course (Entity): Identified by CourseId, contains Title and CourseDuration (VO).
  • Enrollment (Entity/Aggregate): Ties a Student to a Course with status and Grade (VO).
  • Email (VO): Valid email with formatting rules.
  • CourseDuration (VO): Weeks/hours representation with validation.
  • Grade (VO): Letter or percentage with controlled construction.

C# sample: Enrollment aggregate

public sealed class Enrollment
{
    public Guid Id { get; }
    public Guid StudentId { get; }
    public Guid CourseId { get; }
    public DateTime EnrolledOn { get; }
    public EnrollmentStatus Status { get; private set; }
    public Grade Grade { get; private set; }

    private Enrollment(Guid id, Guid studentId, Guid courseId, DateTime enrolledOn)
    {
        Id = id;
        StudentId = studentId;
        CourseId = courseId;
        EnrolledOn = enrolledOn;
        Status = EnrollmentStatus.Active;
        Grade = Grade.NotGraded();
    }

    public static Enrollment Create(Guid studentId, Guid courseId)
        => new Enrollment(Guid.NewGuid(), studentId, courseId, DateTime.UtcNow);

    public void Complete(Grade grade)
    {
        if (Status != EnrollmentStatus.Active)
            throw new InvalidOperationException("Only active enrollments can be completed.");
        Grade = grade;
        Status = EnrollmentStatus.Completed;
        // raise domain event: EnrollmentCompleted
    }

    public void Withdraw(string reason)
    {
        if (Status != EnrollmentStatus.Active) return;
        Status = EnrollmentStatus.Withdrawn;
        // raise domain event: EnrollmentWithdrawn
    }
}

public enum EnrollmentStatus
{
    Active = 1,
    Completed = 2,
    Withdrawn = 3
}

public sealed class Grade : IEquatable<Grade>
{
    public string Letter { get; }
    private Grade(string letter) { Letter = letter; }

    public static Grade NotGraded() => new Grade("N/A");
    public static Grade FromLetter(string letter)
    {
        var normalized = (letter ?? string.Empty).Trim().ToUpperInvariant();
        var allowed = new[] { "A", "B", "C", "D", "E", "F", "N/A" };
        if (!allowed.Contains(normalized)) throw new ArgumentException("Invalid grade.", nameof(letter));
        return new Grade(normalized);
    }

    public bool Equals(Grade other) => other is not null && Letter == other.Letter;
    public override bool Equals(object obj) => Equals(obj as Grade);
    public override int GetHashCode() => Letter.GetHashCode(StringComparison.Ordinal);
    public override string ToString() => Letter;
}

C# sample: Course with duration value object

public sealed class Course
{
    public Guid Id { get; }
    public string Title { get; private set; }
    public CourseDuration Duration { get; private set; }
    public bool IsPublished { get; private set; }

    private Course(Guid id, string title, CourseDuration duration)
    {
        Id = id;
        Title = title;
        Duration = duration;
    }

    public static Course Create(string title, CourseDuration duration)
    {
        if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Title is required.", nameof(title));
        return new Course(Guid.NewGuid(), title.Trim(), duration);
    }

    public void Publish()
    {
        if (IsPublished) return;
        IsPublished = true;
        // event: CoursePublished
    }
}

public sealed class CourseDuration : IEquatable<CourseDuration>
{
    public int Weeks { get; }
    public int HoursPerWeek { get; }

    public CourseDuration(int weeks, int hoursPerWeek)
    {
        if (weeks <= 0) throw new ArgumentOutOfRangeException(nameof(weeks));
        if (hoursPerWeek <= 0) throw new ArgumentOutOfRangeException(nameof(hoursPerWeek));
        Weeks = weeks;
        HoursPerWeek = hoursPerWeek;
    }

    public int TotalHours() => Weeks * HoursPerWeek;

    public bool Equals(CourseDuration other) => other is not null && Weeks == other.Weeks && HoursPerWeek == other.HoursPerWeek;
    public override bool Equals(object obj) => Equals(obj as CourseDuration);
    public override int GetHashCode() => HashCode.Combine(Weeks, HoursPerWeek);
}

Microservices and bounded contexts

Each bounded context can map to a microservice with its own model and database. Communication across contexts happens via APIs or domain events. Protect each model from foreign concepts using translation layers or anti-corruption layers (ACLs).

  • Autonomy: Each service decides its storage, scaling, and release cadence independently.
  • Event-driven collaboration: Emit domain events (e.g., EnrollmentCompleted) to inform other contexts like Billing or Reporting.
  • Eventual consistency: Use outbox and idempotent handlers to keep data consistent across boundaries.
  • API contracts: Keep them stable; translate into local models rather than leaking foreign models internally.

Sample events

public sealed record EnrollmentCompletedEvent(Guid EnrollmentId, Guid StudentId, Guid CourseId, DateTime CompletedOn);
public sealed record CoursePublishedEvent(Guid CourseId, string Title, DateTime PublishedOn);

Mnemonics for teaching and recall

EVA RAPS

  • E — Entity: Identity-first, mutable, behavior-rich.
  • V — Value Object: Immutable, equality by values, validation at the edges.
  • A — Aggregate: Consistency boundary and transactional unit.
  • R — Repository: Access to aggregate roots only.
  • A — Application Service: Orchestration of use cases; delegates business rules to domain.
  • P — Policy: Encapsulated business rule (often a domain service).
  • S — Specification: Composable business predicates for selection/validation.

AIM-USE for value objects

  • A — Atomic: Single, coherent concept.
  • I — Immutable: Construct once; use forever.
  • M — Measurable: Comparable, combinable, or constrained.
  • U — Uniquely by values: No identity, just attributes.
  • S — Small: Lightweight and easy to pass around.
  • E — Expressive: Names and operations reveal intent.

ROOT for aggregates

  • R — Rules inside: Invariants live and hold within the boundary.
  • O — Ownership by root: Only the root manipulates internals.
  • O — Outside by ID: Reference other aggregates by identifiers.
  • T — Transaction boundary: One aggregate per transaction whenever possible.

Testing your domain model

Treat your domain like a library: stable API, strong encapsulation, and clear behavior. Unit tests should focus on business rules, not infrastructure.

  • Arrange via constructor/factory: Avoid setters in tests; use real creation flows.
  • Assert invariants: Verify rules like “cannot confirm empty order” or “grade must be valid.”
  • Event assertions: If you raise domain events, capture and assert them.
  • State transitions: Test lifecycle changes (active → completed → archived) and their constraints.

C# sample test pseudo-code

// using your preferred test framework
var order = Order.Create(customerId);
order.AddLine(productId, new Money(50, "INR"), 2);
order.Confirm();

Assert.True(order.IsConfirmed);
Assert.Equal(new Money(100, "INR"), order.Total());
Assert.Throws<InvalidOperationException>(() => order.AddLine(productId, new Money(50, "INR"), 1));

Common pitfalls and remedies

Pitfall Symptoms Remedy
Anemic domain model Entities with only getters/setters; logic in services/controllers Move business rules into entities/value objects; keep services thin
Over-sized aggregates High contention, large transactions, sluggish writes Split by invariant boundaries; reference others by ID
Primitive obsession Strings and decimals everywhere; scattered validation Introduce value objects for money, email, durations, quantities
Leaky boundaries Holding references to internals; bypassing root rules Expose read-only views; mutate only via aggregate root
Misaligned language Code names differ from business terms; confusion in meetings Adopt the ubiquitous language; refactor code to match
Premature microservices Distributed monolith with brittle contracts Start modular monolith with bounded contexts; extract carefully

Practical guidelines and packaging

  • Start with conversations: Run domain discovery sessions; harvest terms and rules into a living glossary.
  • Sketch context maps: Name contexts, define relationships, and agree on integration patterns.
  • Model purposefully: Start with entities/VOs and their behaviors; avoid database-first thinking.
  • Capture invariants early: Write them down and enforce in code; invariants drive aggregate boundaries.
  • Iterate with feedback: Show model examples to domain experts; refine names and rules continuously.

Solution structure (modular monolith)

src/
  Sales/
    Sales.Domain/
      Orders/Order.cs
      Orders/OrderLine.cs
      Customers/Customer.cs
      ValueObjects/Money.cs
      ValueObjects/Email.cs
      Events/OrderConfirmedEvent.cs
      Repositories/IOrderRepository.cs
    Sales.Application/
      Commands/CreateOrder.cs
      Handlers/CreateOrderHandler.cs
      Services/OrderAppService.cs
    Sales.Infrastructure/
      Persistence/OrderRepository.cs
      Migrations/...
  Shared/
    Shared.Domain/
      ValueObjects/Address.cs
      Abstractions/IDomainEvent.cs
    Shared.Infrastructure/
      Messaging/Outbox/...
  • Clear separation: Domain has no dependencies on application/infrastructure.
  • Shared kernel: Only truly shared concepts become shared; prefer duplication over forced coupling.
  • Migration path: Extract bounded contexts into services when seams are stable and justified.

Cheat sheet: Entity vs. value object

Dimension Entity Value object
Identity Stable ID across time No ID; defined by attributes
Mutability Mutable; state changes in place Immutable; new instance on “change”
Equality By ID By values
Use cases Customers, Orders, Enrollments Money, Email, Address, Duration
Persistence mapping Tables with primary keys Owned/embedded types

Identification heuristics

  • Ask about sameness: If “same” matters beyond attributes, it’s an entity; if not, it’s a value object.
  • Ask about lifecycle: If it moves through statuses over time, likely an entity.
  • Ask about operations: If arithmetic, normalization, or formatting are core, favor VO.
  • Ask about references: If other aggregates refer to it by ID, it’s an entity.

Glossary

  • Bounded context: A semantic boundary where a particular model and language are valid.
  • Ubiquitous language: A shared, precise vocabulary used in code and conversations.
  • Entity: A mutable domain object defined by identity.
  • Value object: An immutable domain object defined solely by values.
  • Aggregate: A cluster of domain objects with a root and invariants.
  • Repository: An abstraction for persisting and retrieving aggregate roots.
  • Domain service: A stateless operation modeling behavior that spans entities/aggregates.
  • Domain event: A notification that something significant occurred in the domain.
  • Anti-corruption layer: A translation layer that protects a model from external influences.

Conclusion

DDD centers the conversation on what the business actually means, then carries that meaning into code through entities and value objects. Entities anchor identity and lifecycle; value objects make rules explicit and safe. Aggregates enforce invariants; domain services capture cross-cutting rules. Whether you stay within a modular monolith or scale into microservices, the essence is the same: protect your language, make invariants explicit, and keep boundaries clear.

Use the examples here as a starting point. Tighten the ubiquitous language with your domain experts, refine the models, write tests that mirror the business rules, and let your code read like a story the domain can recognize.

Back to Index
Previous Agile-and-QA-Metrics Security in NET Core Next
*