Skip to footer content
Iron Academy Logo
Learn C#
Learn C#

Other Categories

Double Dispatch C#: When Injecting Dependencies Makes Sense – Explained via Derek Comartin's Domain-Driven Design Example

Derek Comartin
8m 44s

In the field of C# programming language, double dispatch is often misunderstood or underutilized. It's a powerful technique that enables polymorphic behavior between two objects involved, especially when handling behavior across derived classes.

In his video "Double Dispatch in DDD: When Injecting Dependencies Makes Sense", Derek Comartin breaks down how double dispatch fits perfectly into Domain-Driven Design (DDD). We'll explore his examples in depth, showing how it can reduce maintenance burden, simplify existing code, and even mimic patterns like the visitor pattern commonly seen in other languages.

If you’ve ever dealt with limitations of single dispatch in C#, or tried to determine behavior based on both the instance of an object and an injected strategy or rule, then this article will help clarify how double dispatch C# can be used effectively.

Why Injecting Behavior Makes Sense in Domain Models

Derek begins by pointing out a common dogma in DDD—that your domain model should have zero dependencies. But that’s not always practical or useful. When modeling behavior, you may need to inject logic such as rules, policies, or strategies. This is where double dispatch helps: you can pass a domain concept, like a policy, into the domain object, and let the method-based evaluation be handled externally—yet with the domain object still having final control.

This pattern makes sense when you consider what you're coupling to: domain behavior, not infrastructure.

The Bad Example: Hardcoded Logic in the Domain

To demonstrate the problem, Derek starts with a Shipment class that contains hardcoded logic to determine lateness:

public bool IsLate(DateTime expectedDelivery)
{
    return _systemClock.Now() > expectedDelivery;
}
public bool IsLate(DateTime expectedDelivery)
{
    return _systemClock.Now() > expectedDelivery;
}

This existing code is simple but inflexible. It relies on compile-time decisions and tightly couples behavior to the class. Changing the rule would require changing the domain model, and testing is more difficult since it’s time-dependent.

The Refactor: Using Policies and Double Dispatch

Derek introduces the interface IDeliveryTimingPolicy:

public interface IDeliveryTimingPolicy
{
    bool IsLate(Shipment shipment);
}
public interface IDeliveryTimingPolicy
{
    bool IsLate(Shipment shipment);
}

He then creates two implementations:

  1. StandardDeliveryTimingPolicy

  2. BufferedDeliveryTimingPolicy

These classes take in the Shipment and return a boolean based on their rule. Here’s a following code snippet from the BufferedDeliveryTimingPolicy:

public bool IsLate(Shipment shipment)
{
    return _clock.Now() > shipment.DeliveryDate.AddMinutes(30);
}
public bool IsLate(Shipment shipment)
{
    return _clock.Now() > shipment.DeliveryDate.AddMinutes(30);
}

Now in the Shipment class, we use double dispatch:

public bool IsLate(IDeliveryTimingPolicy policy)
{
    return policy.IsLate(this);
}
public bool IsLate(IDeliveryTimingPolicy policy)
{
    return policy.IsLate(this);
}

This is classic double dispatch: the first dispatch is calling IsLate() on the shipment and passing the policy; the second dispatch is calling IsLate() on the policy with the shipment as a parameter. Two objects are involved, each taking part in determining behavior—something single dispatch cannot achieve.

Benefits in Testing and Flexibility

This approach results in highly testable and deterministic code. Derek shows examples using standard and buffered policies, where test data is controlled and the runtime type of each object determines the behavior.

var policy = new BufferedDeliveryTimingPolicy(TimeSpan.FromMinutes(30));
var shipment = new Shipment { DeliveryDate = DateTime.UtcNow.AddMinutes(-15) };
Assert.False(shipment.IsLate(policy)); // Because of 30-minute buffer
var policy = new BufferedDeliveryTimingPolicy(TimeSpan.FromMinutes(30));
var shipment = new Shipment { DeliveryDate = DateTime.UtcNow.AddMinutes(-15) };
Assert.False(shipment.IsLate(policy)); // Because of 30-minute buffer

This demonstrates how runtime behavior can be altered without changing the domain model. You get polymorphic behavior based on the policy's dynamic nature, while maintaining domain integrity.

Double Dispatch vs Visitor Pattern in C

Derek’s example resembles the visitor pattern, where you define a set of operations to perform on objects without changing their structure. Typically, in the visitor pattern, you'd see something like:

public abstract class Shape
{
    public abstract void Accept(IVisitor visitor);
}

public class Circle : Shape
{
    public override void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}
public abstract class Shape
{
    public abstract void Accept(IVisitor visitor);
}

public class Circle : Shape
{
    public override void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}

Here, Accept() and Visit() methods are coordinated across two types—the object and the visitor—just like Derek’s use of Shipment and IDeliveryTimingPolicy. This pattern helps implement multiple dispatch behavior even in a single dispatch language like C#.

Composing Rules with Collections of Policies

In another following example, Derek shows how multiple rules can be evaluated via a collection. He introduces rules such as:

  • HasValidDestinationRule

  • AllPackagesPackedRule

  • NotAlreadyShippedRule

Each implements IShipmentReadinessRule with:

public bool IsSatisfiedBy(Shipment shipment)
public bool IsSatisfiedBy(Shipment shipment)

Then, the Shipment class evaluates them like so:

public bool CanShip(IEnumerable<IShipmentReadinessRule> rules)
{
    return rules.All(rule => rule.IsSatisfiedBy(this));
}
public bool CanShip(IEnumerable<IShipmentReadinessRule> rules)
{
    return rules.All(rule => rule.IsSatisfiedBy(this));
}

This void accept pattern allows you to evaluate multiple domain rules dynamically. If you store rules in configuration (especially in a multi-tenant app), you can create a new list at runtime and pass it to the shipment.

Dynamic Rules in Multi-Tenant Applications

Derek points out that in multi-tenant applications, the set of rules might change per customer. You could fetch a list of policies from storage and inject them dynamically:

var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);
var rules = LoadRulesForCustomer(customerId);
var canShip = shipment.CanShip(rules);

This shows how dynamic dispatch and runtime decision-making can be layered into your application. The desired behaviour is achieved by changing the configuration, not the model.

Clarifying Misconceptions: Not All Dependencies Are Bad

Toward the end, Derek addresses the misconception that injecting anything into a domain is bad. He emphasizes that what matters is what you’re injecting. Injecting domain behavior, like specifications or policies, is not the same as injecting infrastructure.

The domain model remains the entry point. It owns the decision but can delegate it to other objects—as long as they are within the same domain context.

Wrapping Up: Why Double Dispatch C# is Powerful in DDD

Derek finishes by urging us to think critically: don’t be afraid of void visit, public virtual void accept, or similar patterns when they bring clarity and maintainability. When you're injecting business logic in a controlled way, you gain flexibility and precision.

So, whether you’re working on a new class or refactoring an existing codebase, double dispatch C# gives you a clean way to separate concerns while maintaining domain focus.

If you’re using policies, specifications, or even building a visitor pattern without realizing it, you’re already close to implementing double dispatch. Understanding it gives you more control over how code behaves at run time, improving testability and adaptability.

Conclusion

To sum it up: double dispatch in C# can be an elegant and practical solution to inject domain logic, preserve encapsulation, and support flexible behavior. When used with patterns like visitor, multiple dispatch, and dynamic keyword usage (carefully), it enables writing expressive, robust domain models.

So next time you call shipment.IsLate(policy)—know that you're leveraging a fundamental pattern that brings C# closer to truly polymorphic design.

Example Tip: If you're creating a PurchaseOrder class and want to determine if an Item can be added based on a policy, try passing the policy to the PurchaseOrder—and let the policy Visit the order. That’s double dispatch in action.

Watch full video on his YouTube Channel and gain more insights on the topic.

Hero Worlddot related to Double Dispatch C#: When Injecting Dependencies Makes Sense – Explained via Derek Comartin's ...
Hero Affiliate related to Double Dispatch C#: When Injecting Dependencies Makes Sense – Explained via Derek Comartin's...

Earn More by Sharing What You Love

Do you create content for developers working with .NET, C#, Java, Python, or Node.js? Turn your expertise into extra income!