3

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());
}

Ced
  • 286
  • 1
  • 8

2 Answers2

5

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 :)

Robert Bräutigam
  • 11,473
  • 1
  • 17
  • 36
  • I've lost one thing in the explanation and slides: without repository, where is `retrieveCustomer(...)`? Where does its SQL belong? – bigstones Oct 02 '22 at 09: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. – Robert Bräutigam Oct 02 '22 at 11:48
  • Are there English language resources other than the slide deck? – VoiceOfUnreason Oct 03 '22 at 22:57
  • There are a [couple of articles](https://javadevguy.wordpress.com/) of mine about this and other similar topics. There is a book called [Object Thinking](http://davewest.us/product/object-thinking/) from David West, which is the best book to date on OO. Other than that, I'm not aware of any other resources specifically discussing connecting OO with DDD. – Robert Bräutigam Oct 04 '22 at 07:07
  • @RobertBräutigam Thanks for the book. Regarding the answer I'm accepting it since it was an answer from you I was waiting for. I'm not sure I agree with everything, maybe as I develop in this domain I will. Point 4 is not true in my example, the repository does not have to change. Also regarding Customer , I've the same concerns as bigstones, creating the customer with an id does not retrieve the fields from the DB, unless it's a asynchronous static factory on the Customer itself in which case there might be issues for testing unless the connection can be passed as param to the static factory. – Ced Oct 05 '22 at 23:10
  • @Ced On point 4. Can you introduce a "field", i.e. have more (or less, or different) private state in the Customer, without changing the repository? If yes, then you're ok, I guess. – Robert Bräutigam Oct 06 '22 at 07:15
  • @RobertBräutigam yes because the repository uses `toJson()` and internally diffs against cached json value. That's supported as is by the library I'm using. – Ced Oct 06 '22 at 09:25
  • @RobertBräutigam Also, the question, "How do you load an entity" is very valid. That's the whole point of repository, yet you make a claim that it is possible to live without ? I'm sorry but this doesn't seem to be thought through. You cannot <> (your word) values that live in the DB and you cannot use the constructor to load the values since it's an async operation. That leaves you with the static method option, eg `Product.findOne(id). – Ced Oct 15 '22 at 08:20
  • @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 :) – Robert Bräutigam Oct 15 '22 at 12:16
  • 1
    @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 Oct 17 '22 at 10:23
  • I have converted a module using this approach, so the main class exported by the module actually contains the vertical slice. I really do believe there is something that needs to be dug here. There is a way of doing things that is **demonstrably** better than the others in terms of: cohesion, coupling, encapsulation and combinatorial path and those things are provable. I've analyzed the graph metrics (Jhon Lakos) of the module I converted and this is already numbers that go into that direction. What I'm trying to say is that I don't think this is a matter of opinion and trade offs. – Ced Oct 17 '22 at 10:25
  • @Ced Believe me, I know it's a completely different way of thinking about design. That is why it is so difficult to communicate, it has almost no common vocabulary with traditional approaches. There are some people who at least seem to go in this direction, like David West, Kevlin Henney. Although I did not see any production code from them, so I can't know for sure. It's really cool that you tried it though! – Robert Bräutigam Oct 17 '22 at 11:37
0

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.

bigstones
  • 796
  • 1
  • 8
  • 16
  • 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 Oct 01 '22 at 12:45
  • @Ced: maybe I wasn't clear, I meant *many instances of the same entity*. That's a collection, not an aggregate. https://softwareengineering.stackexchange.com/q/306871/50398 – bigstones Oct 02 '22 at 08: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 Oct 02 '22 at 08:48