4

I practicing DDD concepts on smaller applications and I use to have the same problem when I have to rely on data coming from outside the domain.

In a previous project, when I wrote an appointment scheduler the appointment could not be cancelled after due date 5 am. So cancelling appointments was dependent on the actual time, which we can call an external dependency. As far as I remember I did something like TimeSercice.canBeCancelled(appointment.getDate()), to cover that 5 am rule, but I am not sure if that was the proper solution. It just seemed important enough to include this rule into the domain, especially because it was part of the user stories. Maybe I was wrong.

Now I face something similar. I write an application, which downloads data from an external service every 30 mins. It is something like downloading probabilities for a GPS grid and if the probability by the user's GPS coord reaches a threshold, then it sends a notification to the user, that something is happening. For now I write it as a native mobile application, but I might move most of the code into a webservice later to make it more reliable and reduce the data traffic. What I thought of is doing something like ProbabilityService.probabilityForPosition(user.getPosition()) to get the probability for the user's position. So that would hide the HTTP calls, parsing the data, etc.

I define the interfaces of these services inside the domain, but the implementation is not part of the domain. I use to inject these kind of dependencies. As far as I remember I injected it by the appointment scheduler from the repositories into the domain objects. Is this a valid approach or DDD follows a different logic if it comes to external dependencies? If so, then why?

2 Answers 2

3

Is this a valid approach or DDD follows a different logic if it comes to external dependencies? If so, then why?

Is it valid? yes, in the sense that you will find a number of treatments that seem to do it that way.

But you get something closer to the spirit of "isolated domain layer" if you delegate the retrieval of data from remote processes to the application, and pass that data to the domain model when available.

Which is to say, the primary mechanism for passing new information to the domain model should be command arguments.

In a previous project, when I wrote an appointment scheduler the appointment could not be cancelled after due date 5 am. So cancelling appointments was dependent on the actual time, which we can call an external dependency

For a case like this, our application logic would have two dependencies - one would be the "clock", and the other would be the domain model.

model = repository.get(id)
time = clock.now()
model.onTick(time)

What I thought of is doing something like ProbabilityService.probabilityForPosition(user.getPosition()) to get the probability for the user's position. So that would hide the HTTP calls, parsing the data, etc.

Assuming that user is part of your domain model, the alternate approach would look like:

pos = user.getPosition()
probability = ProbabilityService.probabilityForPosition(pos)
user.updateProbability(probability)

Again, the application is doing the orchestration between our remote data collection capabilities and the domain model calculations.

If you watch Cory Benfield discussion how to design protocol libraries, you'll see this idea within it.

2
  • If I check your time example, it would be better to have a TimeRepository and a single time entity, that we change regularly from the outside, maybe with a saga?. So for example when time changed and we reached the apt deadline, we can trigger apt locking instead of checking the time when somebody wants to cancel the apt. It would be somewhat more expensive because of the regular checks, but we would not need to check the time, just the locked status, when we would display the apt to the user and the cancel button would not even show up when it is locked, which means better user experience
    – inf3rno
    Commented Dec 12, 2019 at 4:28
  • As of the grid, we can do the same, we can have a single grid entity and when that changes, we can go through all user entities and change the probabilitiy for each of them, or when a single user position changes, then we can get a new probability from the grid for the new position. Again this might need more resources and thinking, than the service approach, but it is better, because for example when the user position does not change, we can forget to update the probability, which might have changed if we got new data from the external service meanwhile.
    – inf3rno
    Commented Dec 12, 2019 at 4:34
2

The core concept of DDD is that an application should be built using the language of the problem. This is at the core of the design concept, and it nicely meshes with object-orientation. So let's concentrate on that with your first example:

the appointment could not be cancelled after due date 5 am

You wrote:

TimeSercice.canBeCancelled(appointment.getDate())

Now which of those words makes sense to somebody who knows the problem, but not specifically your solution to it: TimeService, canBeCancelled, appointment, getDate. It would think 1 out of 4, namely appointment. You could maybe argue canBeCancelled.

So why not do:

appointment.cancel();

Looks much more readable and understandable. Both words from the problem domain. In this method you can check for current time. Inject a Clock:

public final class Appointment {
   private final Clock clock;
   private final Date startTime;
   ...
   public void cancel() {
      if (clock.sameDayAs(startTime) && clock.after(...5am...)) {
         ...
      }
      ...
   }
}

Your second example:

ProbabilityService.probabilityForPosition(user.getPosition())

As far as I can tell the actual function you want is:

user.checkPosition();

All others are implementation detail. It is both readable and at the same time hides how that is achieved. You can change the internal implementation to webservice calls, or even create different implementations of the User itself.

Summary

It seems there is a fundamental assumption in your solutions that there can be no external dependencies in the "domain", it must be completely "pure". This is neither true nor practical.

10
  • In the first example it would be good to have a canBeCancelled method, so it won't show the cancel button to the user if the appointment cannot be cancelled, but it really depends on whether we want to do CQRS or this code will used both for read and write.
    – inf3rno
    Commented Dec 12, 2019 at 8:59
  • As of the second example what I really need is checking the probability for the position of the user using a position->probability grid, and when it reaches a threshold, then send a notification to the user. I update the grid using an external service. I think the approach Voice suggested is better for that problem and I would go even further and include the concept of the grid as an entity in my model. Maybe I can even make it an aggregate root and do something like grid.update(dataFromExternalService) or grid.moveUser(userId, position).
    – inf3rno
    Commented Dec 12, 2019 at 9:08
  • "It seems there is a fundamental assumption in your solutions that there can be no external dependencies in the "domain", it must be completely "pure". This is neither true nor practical." - Can you send a reference? I mean I read a few times that the model should not include external things, because calling an external service can result in errors and error handling would make the model complicated. In both cases it is possible to workaround this by updating something inside the domain using a loop and that loop calls the external service from outside the domain.
    – inf3rno
    Commented Dec 12, 2019 at 9:11
  • I am aware that there are a lot of articles and even books out there that want the "model" to be "pure" in the sense that it is essentially just data, without any functionality. Maybe that is also what you want. I'm merely pointing out, this is not the only option, and probably not the best option. Most articles and books on object-orientation should be a good reference why this is not a good path to take. OO-DDD, Object Thinking Commented Dec 12, 2019 at 9:26
  • 1
    Instead of grid.moveUser(userId, position) you could probably encapsulate the grid within the user, so the call becomes user.moveTo(position)
    – Rik D
    Commented Dec 12, 2019 at 12:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.