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.,
Addressinstead of scatteringStreet,City,ZipCodefields 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
✅ 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
✅ 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
✅ 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