3
votes

Background

Suppose I am tasked with building a system in the domain of notification sending using Domain Driven Design (DDD). One of the key requirements of this system is that it needs to support various "types" of notifications, such as SMS, email, etc.

After several iterations on developing the domain model, I continue to land on having a Notification base class as an entity, with subclasses SMSNotification, EmailNotification, etc. as child classes (each being an entity as well).

Notification

public abstract class Notification extends Entity<UUID> {
    //...fields...

    public abstract void send();
}

SMSNotification

public class SMSNotification extends Notification {

    public void send(){
         //logic for sending the SMS notification using an infrastructure service.
    }
}

EmailNotification

public class EmailNotification extends Notification {

    public void send(){
        //logic for sending the email notification using an infrastructure service.
    }
}

Problem(s)

  • With this current design approach, each subclass of Notification is interacting with an infrastructure service, where the infrastructure is tasked with interfacing with some external system.

Eric Evans dedicates a little page space about this on page 107 in his book Domain-Driven Design when introducing the concept of domain services:

..., in most development systems, it is awkward to make a direct interface between a domain object and external resources. We can dress up such external services with a facade that takes inputs in terms of the model, ... but whatever intermediaries we may have, and even though they don't belong to us, those services are carrying out the domain responsibility...

  • If instead, I procure a SendNotificationService in my domain model using Evans' advice instead of having a send method on each subclass of Notification, I am not sure how I can avoid the need for knowing what type of notification was provided, so that the appropriate infrastructure action can be taken:

SendNotificationService (Domain Service)

public class SendNotificationService {
    public void send(Notification notification){
        //if notification is an SMS notification...
        //    utilize infrastructure services for SMS sending.
        //if notification is an email notification...
        //    utilize infrastructure services for email sending.
        //
        //(╯°□°)╯︵ ┻━┻)
    }
}

What am I missing here?

  • Object oriented design principles are pushing me in favor of having the model first suggested, with the Notification, SMSNotification, and EmailNotification classes. Implementing the send method on each subclass of Notification makes sense, as all notifications need to be sent (justifies its placement in Notification) and each "type" or subclass of Notification will have specialized behavior in how the notification is sent (justifies making send abstract in Notification). This approach also honors Open/Closed Principle (OCP), since the Notification class will be closed to modification, and as new notification types are supported, a new subclass of Notification can be created to extend functionality. Regardless, there seems to be consensus on not having entities interface with external services, as well as not having subclasses of entities at all in DDD.
  • If the behavior of sending notifications is removed from Notification, then where it is placed must be aware of the "type" of notification, and act accordingly, which I can only conceptualize as chain of if...else... statements, which directly contradicts OCP.
3
The answer is: never inject anything inside the domain entity. Domain entity should take care of It's own logic. Every other logic that doesn't fit inside the entity should be implemented inside a domain service, which should be very rare. If you want to reach this kind of approach, where the entity is responsible for notifying when something happens, you should move to an event-based architecture, where a Domain Event, and Domain Handler fires the Notification. - Fals
@FreerFactor Domains that are tightly coupled to technical abilities are among the most tricky to model. The 3 lines of description of your business problem don't tell us what you intend to do with these notifications beyond sending them, what are the use cases to act on them, and ultimately whether to use the DDD tactical patterns or something else. - guillaume31
If we stick to your description of the problem domain, you could use simple CRUD or even rely on the external technical platforms' logging and auditing and walk away just as happy with a nice history of your notifications. - guillaume31
@guillaume31 I've had the same questions with document templates where a DocumentTemplate has some state but most behaviors are technical. I've wondered if I should do something like docTemplate.generate(service), service.generate(docTemplate) or even having a docTemplate hold a reference to a TemplateFile interface implemented in the infrastructure where there's one for every kind of template (direct service reference)? If I'm not modeling a domain model at all, should I just use plain result sets and all logic into services? Can't think without domain models anymore... - plalx
Maybe entities (good) vs services (bad) is the wrong dichotomy and DDD the wrong angle here. Does it even make sense to call a DocumentGenerator a "service"? If the template management part is very simple, couldn't you model it as CRUD and implement the technical generation part as plain old non-DDD objects? If the template part is complex, why not two BC's - one for templates and one for generation? - guillaume31

3 Answers

1
votes

TLDR: If you need some infrastructure logic to be executed against your domain and you need some input to it from domain - don't build it in, just declare intentions with appropriate data/markers. You'll then process this declared intentions later, in infrastructure layer.

Do notifications of various kind differ in any way other that delivery mechanism? If not - there could be enough to use a Notification value object (or Entity, if your domain model requires so) with additional field (Enum, if the list is known, or some kind of marker) to store a delivery method name. Maybe, there could be numerous such methods per single notification instance.

Then you have a business logic - a domain service - to fire a notification. A domain service should only depend on domain vocabulary. E.g NotificationDeliveryMethodProvider.

In your adapters layer you can implement various delivery method providers to interact with infrastructure. And a factory to get providers according to a value in DeliveryMethod enum (or marker).

Basically, it's not an aggregate's responsibility to "send" itself of manipulate in any way. Its responsibility should be to maintain its state, execute state transitions in a consistent way and coordinate states of its enclosed entities/values. And fire events about its state changes.

In one of my projects I used the following subpackages under my domain package:

  • provides - interfaces of domain services provided to clients
  • cousumes - interfaces of upstream dependencies
  • businesslogic - implementation of domain services
  • values - value objects with code to enforce their invariants
  • ...

Besides domain package there were also:

  • adapters package dealing with infrastructure
  • App object, where all interfaces were bound to implementations.
  • [There could also be] config package, but in my case it was very light.

These domain, adapters, App and config could be deployed as different jar-files with clear dependency structure, if you need to enforce it for somebody other.

0
votes

I agree with you that the main responsibility of a Notification should be, that it can send itself. That is the whole reason it exists, so it's a good abstraction.

public interface Notification {
    void send();
}

The implementations of this interface are the infrastructure services you are looking for. They will not (should not) be referenced directly by other "business" or "core" classes.

Note about making in an Entity: My own takeaway from reading the blue book is, that DDD is not about using Entity, Services, Aggregate Roots, and things like that. The main points are Ubiquitous Language, Contexts, how to work the Domain itself. Eric Evans himself says that this thinking can be applied to different paradigms. It does not have to always involve the same technical things.

Note about the "conventional" design from the other comment (@VoiceOfUnreason): In object-orientation at least, "holding state" is not a real responsibility. Responsibilities can only directly come from the Ubiquitous Language, in other words from the business. "Conventional" (i.e. procedural) design separates data and function, object-orientation does exactly the opposite. So be sure to decide which paradigm you are aiming for, then it may be easier to choose a solution.

0
votes

After several iterations on developing the domain model, I continue to land on having a Notification base class as an entity, with subclasses SMSNotification, EmailNotification, etc. as child classes

That's probably an error.

public abstract class Notification extends Entity<UUID> {
    public abstract void send();
}

That almost certainly is. You can make it work, if you twist enough, but you are going the wrong way around.

The responsibility of the entities in your domain model is the management of state. To also have the entity be responsible for the side effect of dispatching a message across your process boundary violates separation of concerns. So there should be a collaborator.

For Evans, as you will have noted, the collaboration takes the form of a domain service, that will itself collaborate with an infrastructure service to produce the desired result.

The most straight forward way to give the entity access to the domain service is to simply pass the domain service as an argument.

public class SMSNotification extends Notification {
    public void send(SMSNotificationService sms) {
        //logic for sending the SMS notification using an infrastructure service.
    }

The SMSNotification supports a collaboration with an SMSNoticationService provider, and we make that explicit.

The interface you've offered here looks more like the Command Pattern. If you wanted to make that work, you would normally wire up the specific implementations in the constructor

public class SMSCommand extends NotificationCommand {
    private final SMSNotificationService sms;
    private final SMSNotification notification;

    public final send() {
        notification.send(sms);
    }
}

There are some things you can do with generics (depending on your language choice) that make the parallels between these different services more apparent. For example

public abstract class Notification<SERVICE> extends Entity<UUID> {
    public abstract void send(SERVICE service);
}

public class SMSNotification extends Notification<SMSNotificationService> {
    public void send(SMSNotificationService service){
        //logic for sending the SMS notification using an infrastructure service.
    }
}

public class NotificationCommand<SERVICE> {
    private final SERVICE service;
    private final Notification<SERVICE> notification;

    public final send() {
        notification.send(service);
    }
}

That's the main approach.

An alternative that sometimes fits is to use the poor man's pattern match. Instead of passing in the specific service needed by a particular type of entity, you pass them all in....

public abstract class Notification extends Entity<UUID> {
    public abstract void send(SMSNotificationService sms, EmailNotificationService email, ....);
}

and then let each implementation choose precisely what it needs. I wouldn't expect this pattern to be a good choice here, but it's an occasionally useful club to have in the bag.

Another approach that you will sometimes see is to have the required services injected into the entity when it is constructed

SMSNotificationFactory {
    private final SMSNotificationService sms;

    SMSNotification create(...) {
        return new SMSNotification(sms, ...);
    }
}

Again, a good club to have in the bag, but not a good fit for this use case -- you can do it, but suddenly a lot of extra components need to know about the notification services to get them where they need to be.

What's best between notification.send(service) and service.send(notification)

Probably

notification.send(service)

using "Tell, don't ask" as the justification. You pass the collaborator to the domain entity, and it decides (a) whether or not to collaborate, (b) what state to pass to the domain service, and (c) what to do with any state that gets returned.

SMSNotification::send(SMSNotificationService service {
    State currentState = this.getCurrentState();
    {
        Message m = computeMessageFrom(currentState);
        service.sendMessage(m);
    }
}

At the boundaries, applications are not object oriented; I suspect that as we move from the core of the domain toward the domain, we see entities give way to values give way to more primitive representations.

after reading a bit on pure domain models and the fact there shouldn't be any IO in there I'm not sure anymore

It is, in truth, a bit of a tangle. One of the motivations of domain services is to decouple the domain model from the IO -- all of the IO concerns are handled by the domain service implementation (or more likely, by an application/infrastructure service that the domain service collaborates with). As far as the entity is concerned, the method involved is just a function.

An alternative approach is to create more separation between the concerns; you make the orchestration between the two parts explicit

List<SMSRequest> messages = domainEntity.getMessages();
List<SMSResult> results = sms.send(messages)
domainEntity.onSMS(results)

In this approach, all of the IO happens within the sms service itself; the interactions with the model are constrained to in memory representations. You've effectively got a protocol that's managing the changes in the model and the side effects at the boundary.

I feel that Evans is suggesting that service.send(notification) be the interface.

Horses for courses, I think -- passing an entity to a domain service responsible for orchestration of multiple changes within the model makes sense. I wouldn't choose that pattern for communicating state to/from the boundary within the context of a change to an aggregate.