Skip to content

Double Dispatch in DDD

Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


What’s Double Dispatch? Well, before we get to what it is, there is a common belief in domain-driven design that you want to keep your domain pure, meaning no dependencies, no services, no distractions. I get it because you do not want that core logic coupled to infrastructure concerns like database calls. You want it to be deterministic because you want it to be testable.

But somewhere along the way this advice turned into dogma that you cannot inject behavior into your domain. I am going to challenge that. If you are modeling your domain and capturing behavior, you can inject behavior into your domain using double dispatch. Used correctly, you can write expressive, testable code in the form of policies and specifications. It might actually be the most DDD thing you can do.

YouTube

Check out my YouTube channel, where I post all kinds of content on Software Architecture & Design, including this video showing everything in this post.

Bad example

To start, imagine a simple Shipment. We inject a system clock and have one method, isLate. The behavior is simple. We have a parameter of expected delivery and we compare it with the system clock. If now is greater than expected delivery then it is late.

This is straightforward, but the rule is hardcoded on the Shipment. If the rule needs to vary, the Shipment is the wrong place to embed all those variations.

Refactor using double dispatch

Double dispatch is when an object does not act on its own data. Instead, it delegates the decision to another object. In DDD we can use policies and specifications to model that delegation.

For example, create an interface IDeliveryTimingPolicy. Implement two policies. The first is StandardDeliveryTiming which has an isLate method. Instead of Shipment pulling the current time itself, the policy gets passed a DateTime that represents now. The policy then compares now to the Shipment delivery date to determine lateness.

The second is BufferDeliveryTiming. It takes a buffer value, maybe minutes, hours, or days. When checking lateness it compares now to the Shipment delivery date plus the buffer. So if a delivery date was right now and the buffer is 30 minutes, the shipment is not late until after those 30 minutes.

Here is where the double dispatch happens. When I call shipment.isLate(policy), I pass the policy into the Shipment. That policy then receives the Shipment as a parameter and makes the determination. The Shipment delegates the decision, and the policy acts using the Shipment data. Shipment still owns the question of whether it is late, but it delegates the mechanics of the rule.

This allows the domain to remain expressive and testable. The rule itself lives in the policy. The domain is the entry point for the decision, but the policy defines the rule.

Testing is deterministic

Testing remains straightforward because everything is deterministic. You create the Shipment and you create the policy with a specified now value. For example, create a Shipment with a delivery date of yesterday and test it with the StandardDeliveryTiming policy where now is today. It is late.

Or create a Shipment with a delivery date of 15 minutes ago and test it with a BufferDeliveryTiming policy that adds a 30 minute buffer and uses the current time. It is not late because of the buffer. Both tests are deterministic. You set up the Shipment and the policy and assert the outcome.

Specifications and collections of rules

Double dispatch applies to more than just timing policies. Consider shipment readiness. Define a ShipmentReadinessRule interface with isSatisfiedBy and pass the Shipment into it.

  • HasValidDestination, checking that the Shipment has a destination.
  • AllPackagesPacked, iterating through packages to ensure each is marked packed.
  • NotAlreadyShipped, checking the Shipment status.

Now create a CanShip method that takes an enumerable collection of these rules and verifies they all pass. The Shipment still controls whether it can ship, but it delegates each individual rule to a specification object.

Configurable rules in multi tenant systems

In a multi tenant SaaS application these rules are often configurable. How do you decide which rules to pass in? Typically you load configuration from storage and build the rule set at the application layer. For example, get the Shipment, determine the customer, fetch that customer’s configured rules, build the specifications and pass them to CanShip.

Testing follows the same pattern. Build the Shipment and the rules and assert the results. Because everything is passed in as explicit objects with explicit inputs, tests are deterministic and clear.

Addressing the dogma

Is injecting something into your domain via constructor injection or as an argument to a domain method always terrible? No. Not when you are passing domain behavior and domain concepts. You are not passing a database or a logger. You are passing policies and specifications, things that belong to the problem space.

At the core, the real reason people avoid dependencies is coupling. What are you coupling to? Are you coupling to domain concepts or infrastructure concerns? There is a big difference. Inject domain concepts when it makes sense. The domain can delegate parts of the decision making without losing ownership.

Do not inject anything into your domain

If that is the mantra you were taught, consider this. The policy you pass in is a domain rule. You are putting behavior back into the domain, not moving it out. The Shipment remains the entry point and still owns the decision. It just asks a domain concept for help deciding how to apply the rule.

Double Dispatch

Double dispatch is a simple and powerful way to keep your domain expressive and testable while allowing behavior to be injected in a controlled way. Use policies and specifications when those behaviors are part of the problem space. Avoid injecting infrastructure concerns into the domain. Consider what you are coupling to and keep your domain the owner of the decision, even when it delegates.

Join CodeOpinon!
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.