Skip to content

Value Objects

Info

At ConnectSoft, Value Objects are essential building blocks:
Identity-free, immutable, and behavior-driven, they model pure domain concepts
without the weight of lifecycle management.


Introduction

In Domain-Driven Design (DDD), a Value Object is an object that:

  • Represents a descriptive aspect of the domain (e.g., Address, Money, DateRange).
  • Has no distinct identity — two Value Objects with the same attribute values are considered equal.
  • Is immutable — once created, its state cannot change.
  • May carry behavior — they are not just data holders.

Value Objects are cheap, disposable, replaceable
and critical to building rich, expressive, and maintainable domain models.

At ConnectSoft, modeling Value Objects correctly:

  • Strengthens domain expressiveness.
  • Prevents anemic models.
  • Supports CQRS, Event Sourcing, and resilient microservices architectures.

Concept Definition

Aspect Description
Identity No identity — equality based purely on attribute values.
Immutability Once created, cannot be modified — only replaced.
Behavior Encapsulates domain logic alongside state.
Lifecycle Created and discarded freely — no need for lifecycle tracking.
Examples Address, Money, GeoLocation, DateRange, PhoneNumber.

📚 Why Value Objects Matter at ConnectSoft

Enhance Model Expressiveness

  • Model real-world concepts directly (e.g., Address instead of scattering Street, City, ZipCode fields everywhere).

Promote Immutability and Safety

  • Immutable structures are safer, more predictable, easier to reason about.

Enable Behavior-Rich Domain Modeling

  • Value Objects can validate themselves and encapsulate operations (e.g., Money.Add(), GeoLocation.DistanceTo()).

Reduce Boilerplate

  • No need for IDs, audit fields, lifecycle services, or ORM tracking complexities.

Improve Testability

  • Easy to unit test — pure functions without side effects.

Strengthen Event Sourcing

  • Value Objects are naturally serializable and version-friendly.

Value Objects vs Entities

Aspect Value Object Entity
Identity No identity; equality by attributes Unique identity (Id)
Mutability Immutable Mutable (state can change across time)
Lifecycle Management No need Full lifecycle tracking (Create, Update, Delete)
Persistence Complexity Simple serialization Complex persistence (linked by Ids, versioning)
Use Cases Attributes, Measurements, Descriptors Core business concepts (Customer, Order, Appointment)

📚 Examples of Common Value Objects

Value Object Example Usage
Address BillingAddress, ShippingAddress
Money Amount + Currency
DateRange Booking periods, Subscription durations
GeoLocation Latitude + Longitude coordinates
PhoneNumber Validation and formatting of phone contacts

🧩 Visual: Entity-Value Object Relationship

classDiagram
    class Customer {
      +CustomerId : Guid
      +FullName : string
      +Address : Address
    }

    class Address {
      +Street : string
      +City : string
      +ZipCode : string
    }

    Customer --> Address
Hold "Alt" / "Option" to enable pan & zoom

Customer has a Value Object Address,
not a raw collection of strings — encapsulated behavior and validation.


Strategic Design Principles for Value Objects

At ConnectSoft, Value Objects are modeled tactically
every decision reinforces immutability, equality by value, and clean domain expressiveness.


📚 Core Principles for Strong Value Object Modeling

Immutability by Default

  • Once created, a Value Object’s fields must never change. Updates must result in new instances.

Equality by All Attributes

  • Two Value Objects are equal if all their attributes match exactly — not based on object reference.

Small and Focused

  • A Value Object should model one coherent domain concept, not become a bloated structure.

Self-Validation at Construction

  • Validate correctness (e.g., non-null, proper formats) inside the constructor or factory method.

Include Behavior When Logical

  • Value Objects are not DTOs — they should include relevant business behaviors (e.g., Money.Add(), GeoLocation.DistanceTo()).

Composable and Serializable

  • Design Value Objects to work well with serialization (e.g., JSON) and easy composition into larger Aggregates.

Treat Them as First-Class Citizens

  • In code, events, APIs, storage models — model Value Objects explicitly instead of scattering primitive fields.

📚 Equality Implementation for Value Objects

At ConnectSoft, all Value Objects override Equals and GetHashCode based on attribute values.

Example Equality Template:

public abstract class ValueObject
{
    protected abstract IEnumerable<object> GetEqualityComponents();

    public override bool Equals(object obj)
    {
        if (obj == null || obj.GetType() != GetType())
            return false;

        var other = (ValueObject)obj;
        return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
    }

    public override int GetHashCode()
    {
        return GetEqualityComponents()
            .Aggregate(1, (current, obj) =>
            {
                return current * 23 + (obj?.GetHashCode() ?? 0);
            });
    }
}

✅ All derived Value Objects simply define GetEqualityComponents() listing their fields.


📚 Example: Address Value Object (Equality)

public class Address : ValueObject
{
    public string Street { get; }
    public string City { get; }
    public string ZipCode { get; }

    public Address(string street, string city, string zipCode)
    {
        if (string.IsNullOrWhiteSpace(street) || string.IsNullOrWhiteSpace(city) || string.IsNullOrWhiteSpace(zipCode))
            throw new ArgumentException("Address fields cannot be empty.");

        Street = street;
        City = city;
        ZipCode = zipCode;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Street;
        yield return City;
        yield return ZipCode;
    }
}

Address instances are equal by their field values, not by memory reference.


🛑 Common Anti-Patterns to Avoid with Value Objects

Anti-Pattern Symptom Why It's Dangerous
Mutable Value Objects Allow setters or field changes after creation. Breaks equality, leads to bugs.
Reference-Based Equality Value Objects are compared by object reference. Violates value-based nature, causes subtle logic failures.
Overgrown Value Objects Pack too much unrelated data or behaviors. Violates SRP (Single Responsibility Principle), makes models fragile.
Primitive Obsession Split Value Object fields across Aggregates instead of encapsulating. Increases duplication, weakens model clarity.
Leaky Serialization Details Tie Value Objects tightly to database schemas or ORM configurations. Breaks domain independence, hurts portability.

📚 Good vs Bad Practices for Value Objects

✅ Good Practice 🚫 Bad Practice
Immutable after creation Allow mutating fields after creation
Equality by attribute values Compare by object reference
Model coherent concepts Pack unrelated fields into one VO
Add relevant domain behaviors Make VOs passive DTO-like shells
Explicit modeling across services and storage Use flat primitives everywhere

🧩 Visual: Equality and Lifecycle of Value Objects

sequenceDiagram
    participant User1
    participant User2
    participant Address1
    participant Address2

    User1->>Address1: Create Address(Street A, City B, Zip 12345)
    User2->>Address2: Create Address(Street A, City B, Zip 12345)
    Address1-->>Address2: Equals == true
Hold "Alt" / "Option" to enable pan & zoom

✅ Two Address instances with the same data are equal,
✅ even if they are separate objects in memory.


C# Examples: Advanced Value Object Modeling at ConnectSoft

At ConnectSoft, Value Objects are not passive DTOs
they carry behaviors, validations, and business meaning,
making our domain models more expressive, safe, and evolvable.


🛠️ Example 1: Money Value Object

public class Money : ValueObject
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        if (amount < 0)
            throw new ArgumentException("Amount cannot be negative.");
        if (string.IsNullOrWhiteSpace(currency))
            throw new ArgumentException("Currency is required.");

        Amount = amount;
        Currency = currency.ToUpperInvariant();
    }

    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("Cannot add Money values with different currencies.");

        return new Money(Amount + other.Amount, Currency);
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Amount;
        yield return Currency;
    }
}

✅ Behaviors: Add with currency safety.
✅ Immutability and correct equality implementation.


🛠️ Example 2: DateRange Value Object

public class DateRange : ValueObject
{
    public DateTime Start { get; }
    public DateTime End { get; }

    public DateRange(DateTime start, DateTime end)
    {
        if (start >= end)
            throw new InvalidOperationException("Start date must be before end date.");

        Start = start;
        End = end;
    }

    public bool Overlaps(DateRange other)
    {
        return Start < other.End && other.Start < End;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Start;
        yield return End;
    }
}

✅ Validates start/end logic at construction.
✅ Supports Overlaps behavior — critical for bookings, appointments, etc.


🛠️ Example 3: GeoLocation Value Object

public class GeoLocation : ValueObject
{
    public double Latitude { get; }
    public double Longitude { get; }

    public GeoLocation(double latitude, double longitude)
    {
        if (latitude < -90 || latitude > 90)
            throw new ArgumentOutOfRangeException(nameof(latitude), "Latitude must be between -90 and 90.");
        if (longitude < -180 || longitude > 180)
            throw new ArgumentOutOfRangeException(nameof(longitude), "Longitude must be between -180 and 180.");

        Latitude = latitude;
        Longitude = longitude;
    }

    public double DistanceTo(GeoLocation other)
    {
        var earthRadiusKm = 6371;
        var dLat = DegreesToRadians(other.Latitude - Latitude);
        var dLon = DegreesToRadians(other.Longitude - Longitude);

        var lat1 = DegreesToRadians(Latitude);
        var lat2 = DegreesToRadians(other.Latitude);

        var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
                Math.Sin(dLon / 2) * Math.Sin(dLon / 2) * Math.Cos(lat1) * Math.Cos(lat2);
        var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));

        return earthRadiusKm * c;
    }

    private double DegreesToRadians(double degrees)
    {
        return degrees * Math.PI / 180;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Latitude;
        yield return Longitude;
    }
}

✅ Behavior-rich — computes distance between two points.
✅ Input validation ensures domain integrity.


📚 Lessons from Advanced Modeling

Good Practice Why It Matters
Validate on Construction No invalid Value Objects exist in the system.
Behaviors Belong Inside VOs Reduces service orchestration complexity.
Equality Logic by Attributes Enables safe comparisons and collections usage.
Treat VOs as Core Domain Concepts Strengthens expressiveness and business alignment.
Avoid DTO-Only Mentality Push meaningful behaviors into VOs when appropriate.

📚 Example Usage of Value Objects in Aggregates

public class Subscription
{
    public Guid Id { get; private set; }
    public Money MonthlyFee { get; private set; }
    public DateRange ActivePeriod { get; private set; }

    private Subscription() { }

    public Subscription(Money monthlyFee, DateRange activePeriod)
    {
        Id = Guid.NewGuid();
        MonthlyFee = monthlyFee ?? throw new ArgumentNullException(nameof(monthlyFee));
        ActivePeriod = activePeriod ?? throw new ArgumentNullException(nameof(activePeriod));
    }
}

✅ Richer Aggregates emerge naturally by embedding smart Value Objects.


🧩 Visual: Composing Aggregates with Value Objects

classDiagram
    class Subscription {
      +SubscriptionId : Guid
      +MonthlyFee : Money
      +ActivePeriod : DateRange
    }

    class Money {
      +Amount : decimal
      +Currency : string
    }

    class DateRange {
      +Start : DateTime
      +End : DateTime
    }

    Subscription --> Money
    Subscription --> DateRange
Hold "Alt" / "Option" to enable pan & zoom

✅ Aggregates composed of behavior-rich, validated Value Objects.


Best Practices for Value Objects

At ConnectSoft, Value Objects are essential tactical modeling tools
designed to enhance expressiveness, enforce immutability, and strengthen business alignment.


📚 Best Practices Checklist

Immutability by Default

  • No public setters; values are assigned only at construction time.

Equality by Attributes

  • Implement equality and hash code generation based on all meaningful attributes.

Small, Focused, and Coherent

  • Each Value Object models one clear domain concept.

Validate Upon Creation

  • Ensure invalid instances cannot exist — validate at construction or factory methods.

Include Behavior Where Logical

  • Value Objects should encapsulate domain-specific operations, not just hold data.

Compose Aggregates from Value Objects

  • Build richer, safer Aggregates by composing them with well-modeled Value Objects.

Model Across All Layers

  • Use Value Objects consistently in domain models, API contracts, events, and storage schemas.

Avoid Primitive Obsession

  • Prefer explicit Value Objects (e.g., PhoneNumber, Money, DateRange) over primitive fields.

Serializable and Version-Ready

  • Design for clean serialization, ensuring easy event sourcing or API payload handling.

Refactor and Evolve Language

  • Update Value Objects as domain understanding evolves — they are living domain concepts.

Conclusion

At ConnectSoft, Value Objects are not optional modeling techniques
they are foundations of resilient, expressive, and adaptable systems.

When Value Objects are modeled properly:

  • Domain models become more expressive and business-aligned.
  • Aggregates become simpler, more cohesive, and easier to evolve.
  • Code becomes easier to reason about, test, and extend.
  • Systems become more resilient to change and complexity.

When Value Objects are neglected:

  • Models become anemic, procedural, and fragile.
  • Validation leaks into services instead of being enforced where it matters.
  • Change becomes dangerous and disruptive rather than evolutionary.

At ConnectSoft, we build value-centric systems
where meaning is explicit, business concepts are first-class citizens,
and resilient evolution is designed into every model from the start.

"Value Objects are not filler;
they are the bricks and stones of resilient domain architectures.
"


References

  • Books and Literature

    • Eric Evans — Domain-Driven Design: Tackling Complexity in the Heart of Software
    • Vaughn Vernon — Implementing Domain-Driven Design
    • Jimmy Nilsson — Applying Domain-Driven Design and Patterns
  • Online Resources

  • ConnectSoft Internal Standards

    • ConnectSoft Value Object Modeling Playbook
    • ConnectSoft Resilient Event-Sourcing Design Patterns
    • ConnectSoft Domain Integrity and Validation Guidelines