4

I'm reading OO DDD slides from Robert Brautigam, who is quite active here so I hope he can personally answer as I quite agree with his personal understanding of DDD, and approach of having a more object oriented approach instead of a service one.

In our apps we have an awful lot of this:

class OrderService {
   OrderRepository _orderRepository;

  Future<void> confirmOrder(Order order) {
    final confirmedOrder = order.confirm();  // we use immutable entities
    return _orderRepository.save(confirmedOrder);
  }
}

class DBOrderRepository {
   Future<void> save(Order order) => db.save(order.toJson());
}

The service seems unnecessary because the domain object, Order in this instance, is doing the work to update the fields it needs to and the service just saves the result. For most use cases where there is only one entity involved it seems like the service layer is very thin and might not need to actually exist at all ? That the call could be made directly inside the domain object, which I think is what OP of the slides alludes at. I understand there is an argument for unit testing each part individually and maybe a single responsability principle, but I'm not convinced those 2 arguments hold.

In slides 31 and 32 he gives 2 options. He eliminates the services, but where the entity is saved to persistence is not clear.

In slide 32:

class Order {

Json toJson();
static Order fromJson();

My problem with this is that implementation details are leaking out of the Order, but most importantly the Order is not persisted. You'd still need a service to call the repository eg the same snippet as I have above (it's the method that we currently use):

class OrderService {
   OrderRepository _orderRepository;

  Future<void> confirmOrder(Order order) {
    final confirmedOrder = order.confirm(); 
    return _orderRepository.save(confirmedOrder);   // <== this needs to be called somewhere ?!
  }
}

class DBOrderRepository {
   Future<void> save(Order order) => db.save(order.toJson());
}

2 Answers 2

6

Very short answer: Yeah, sort-of, not quite.

So in OO, objects are not bags of data. So talking about "persisting objects" is already kind-of a non-starter. If I have an object Customer, which has a freezeCreditCards() method, how would I "persist" this object from the outside? What would I even expect this would achieve?

This means the current understanding of what "Repositories" are is already at odds with OO. For several reasons:

  1. The point above about objects not being bags of data.
  2. Even if the object has some internal data that is being changed and even if that data somehow corresponds to some data in the database, there should be no way an external object can just forcefully extract that data from it, be it getters, reflection, or anything else. This would be a complete violation of object boundaries and encapsulation.
  3. Repositories aren't part of the Ubiquitous Language. They are technical artifacts, therefore overhead essentially.
  4. A bit more pragmatically, there is no way our object can change without changing the Repository with it. So things that change together aren't together.

This is what I try to just briefly mention on Slide 29.

Ok, so what now? Well, persistence is a byproduct of a business function. Normally. So when I call freezeCreditCards() I obviously expect that to be permanent/persistent.

But this also means that persistence is not just one thing. One pattern or a generic solution or something like that. Since persistence happens in the context of a business-function, it only makes sense to talk and design persistence in the context of the specific case.

In the slides I list two options that I used in the past:

The toJson() thing is for cases where the requirements demand that we produce some Json to send something over the wire. Integration to other systems. Again, because it is a requirement, it is a business-function, therefore having a public method for it is reasonable.

Again, most of the time persistence is implied. Therefore CRUD methods should not be visible. At all. I give another example I've used in the past, where there is a specific SQL-based implementation of a Customer interface. It fires a direct SQL statement for all behavior it has. I use similar constructs for when behavior needs to go to other services or systems. But again, it is not a generic thing, often it does not fit.

So long answer: The object should do everything to fulfill its implied promises for a given business-function, like persistence. For that, it should talk to any collaborators it has to.

But. These collaborators should not pull data out of our object. That should never happen. So can it have a java.sql.Connection? Sure, why not. Can it have a CustomerDBRepository? No, for the reasons above.

In the talk I also mention that I don't see the point of Services. At all. I went through Vaughn Vernon's Repo to see what he does with Services. I didn't find a single one, where I couldn't find a much better place for it. See example PasswordService. I believe it exists only because "entities" can not be persistent, so they need another "layer" that does it for them. Basically a work-around for a design mistake.

I hope that makes things at least clearer, even if probably not easier.

Update: Under what circumstance would Repositories actually make sense?

If I would have to support multiple databases or had to allow (so the requirements required it) custom implementations, then a Repository-like thing would actually make sense. Note however, that because of the huge costs, I would probably question the need first, before being ok with it :)

12
  • I've lost one thing in the explanation and slides: without repository, where is retrieveCustomer(...)? Where does its SQL belong?
    – bigstones
    Commented Oct 2, 2022 at 9:04
  • 1
    Good question. Because Customer is not a bag of data, it doesn't necessarily need to "load" at all. Specifically in that project, SQLCustomer was actually created (not loaded) with only a customer id. When it was asked to do something, it did it by executing some statement with that id. Again, it is all domain-specific, there is no one generic "pattern". Depending on the domain, sometimes it makes sense to have a factory-like object. Sometimes things like Order and OpenOrders. Even there, the "repo" is not CRUD. It had completeWith(invoice). So it's a business function. Commented Oct 2, 2022 at 11:48
  • Are there English language resources other than the slide deck? Commented Oct 3, 2022 at 22:57
  • 1
    @Ced Thinking about "loading an entity" is dangerous. You don't load objects, you load data that objects need to execute a certain logic. Sure, these get quite similar sometimes, but conceptually they're quite different. I don't need to create values, I only need to create an object for you to start working with. If I don't give you raw data (no object should), but instead demand you tell me what I should do, I can decide what and how to load, or maybe just execute a database statement and not load anything. HTH, it's difficult to answer in just a comment :) Commented Oct 15, 2022 at 12:16
  • 2
    @RobertBräutigam Well you are the only person on the planet that I know of that has this view on architecture and there is no TAG on stackoverflow for this so it's hard to post another question. It's a stark contrast on the status quo, which makes it harder as the gut reaction is "Ugh" when reading about so much coupling between the different "layers" (which as you point out somewhere are often coupled semantically anyway). But it actually makes a lot of sense; and you can still have different files dealing with each layers although those will be private to the domain object.
    – Ced
    Commented Oct 17, 2022 at 10:23
1

In general, a DDD repository is the way you retrieve/persist an entity.

If you intend to have the entity itself be able to interact with the repository, it means that each entity will have a reference to the repository object. Plus, all the "persistence things" you can do will have to be exposed through the entity interface, and that smells of SRP violation.

For instance, imagine having to work on many entities massively, maybe update the same property on each. In the way you propose you'll have to do it one by one, which is not ideal from a performance perspective, and you might even consider creating a specific method on the repository if that's a frequent operation. You wouldn't be able to expose that through the entity interface, in a way that makes sense.

You may still want to expose the "save" method, like it's syntactic sugar... but is it worth it? It's probably mostly a matter of taste.

Regarding the dumb service instead, I totally agree that thin abstraction layers should be avoided if possible. Sometimes you're totally sure you are gonna need it (TM), e.g. to coordinate different repositories or carry out some particular logic. But if it's just CRUD, I'm all for using the repos at the application level (e.g. in REST controllers). In case I'll need a "service" layer, I'll add it with 10 minutes of refactoring.

3
  • 1
    But that's the concept of aggregate. If you have such a thing that requires to update many entities that's an aggregate. I agree with the SRP thing though
    – Ced
    Commented Oct 1, 2022 at 12:45
  • @Ced: maybe I wasn't clear, I meant many instances of the same entity. That's a collection, not an aggregate. softwareengineering.stackexchange.com/q/306871/50398
    – bigstones
    Commented Oct 2, 2022 at 8:28
  • To further explain, my point is that slide 31 is neat and tidy but can lead to N+1 performance hell. If you don't need to operate on collections of objects and you really have such a simple CRUD problem then I guess it's fine. But for simple problems, even Evans recommends to leave DDD aside.
    – bigstones
    Commented Oct 2, 2022 at 8:48

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.