14

I have been struggling with this concept in the context of web applications ever since I first read about it. The theory states that the domain objects should encapsulate their behaviour and business logic. A model which contains only data and has its logic somewhere outside is called an "anemic domain model", which is a bad thing. Also, the domain should not perform data access.

If for instance I had a social app which had a bunch of objects of type User, and users should be able to add other users as their friends, the User class should contain a method named Befriend(User user) so that I could do something like userA.Befriend(userB).

class User {

    Friends[] friends;

    void Befriend(User user) { ... }
}

However, the act of befriending might contain some restrictions and so I would have to do some validation in my Befriend method. Here are some purely theoretical restrictions:

  1. The user must not already be your friend
  2. You and the other user must not have common friends
  3. In Bucharest it must be raining

Now let's imagine that the friends lists might be huge, userA might have 50.000 friends, userB might have 100.000 friends. So, for validating 1 and 2 it wouldn't be efficient to eagerly pull the entire friends lists from the database when constructing the user object and then doing those checks in my Befriend method iterating the friends list. In the database I have indexes and checks like these would be trivial (and fast). So naturally I would prefer to put these queries somewhere in my Data Access Layer and use them whenever needed.

class FriendsRepository: IFriendsRepository {

    bool HasFriend(User user, User friend);
    bool HasCommonFriends(User userA, User userB);

}

But how am I supposed to use this object inside my Befriend method from my User object? People say domain objects must not use repositories (even through abstractions such as interfaces), though there seems to be some disagreement here. Say I violated this rule. Domain objects don't benefit from Dependency Injection so I would have to change my Befriend method to:

void Befriend(User user, IFriendsRepository friendsRepository) { ... }

Alright. Now what about the weather? That's something completely unrelated to our entity and that information comes from an IWeatherService. Again, I need it in my Befriend method.

void Befriend(User user, IFriendsRepository friendsRepository, IWeatherService weatherService) { ... }

This already makes me feel like this method does not belong inside the User class. I have a lot of external dependencies and I don't get Dependency Injection which sucks. But pulling this out from the User to a service (or whatever) inside my Application Layer makes my domain model anemic. I very rarely encountered methods which could either be executed without validation or contain only extremely simple validation rules, only depending on the immediately available properties on the said entity (like primitive fields for instance, such as Username string, ActiveUntil date etc.).

So I'm left asking: what kind of methods could naturally fit in the domain objects? Let's be honest, real apps often deal with huge amounts of data, many object relations and very complex validation logic. Rarely you only have to do trivial checks like "is this user over 12 years old?".

P.S.: I used that example purely for demonstration purposes. Please don't cling on it.

2
  • 4
    As an offtopic aside, I really appreciate the "raining in Bucharest" restriction being added to the example. So often we use simple examples that rely on "common sense" inferences (a good example of a flawed example is the "square vs rectangle" Liskov case), which often omit the sometimes "word of God" nature of the software requirements. So often a theoretical example results in people arguing over the specifics of the example instead of the actual question being posed, and the "raining in Bucharest" restriction really puts a stop to those kinds of distractions.
    – Flater
    Commented Mar 22, 2020 at 16:27
  • 1
    I think the missing piece is a service in your application layer. In your example, I would likely have a FriendService and pass dependencies into that, with a clean befriend(user1, user2) method and perhaps some other friend-related behaviour. The problem you outlined is exactly why I would be sceptical of putting this behaviour in the domain object itself.
    – user9682
    Commented Jan 23, 2022 at 15:49

5 Answers 5

12

Arguably, the smallest method of encapsulation is a function.

float harmonic(int n) 
{ 
    float h = 1.0; 

    for (int i = 2; i <= n; i++) { 
        h += 1.0 / i; 
    } 

    return h; 
}

This function contains both code and data. When the function completes, it returns the data that it contains.

Classes encapsulate code and data in a similar manner. The only real difference is that you can have multiple functions (called "methods" in a class) operating on the same data, and multiple instances of that data.

Consider this partial code listing of a Complex Number class, obtained from here:

public class Complex {
    private final double re;   // the real part
    private final double im;   // the imaginary part

    // create a new object with the given real and imaginary parts
    public Complex(double real, double imag) {
        re = real;
        im = imag;
    }

    // return a new Complex object whose value is (this + b)
    public Complex plus(Complex b) {
        Complex a = this;             // invoking object
        double real = a.re + b.re;
        double imag = a.im + b.im;
        return new Complex(real, imag);
    }

    // return a new Complex object whose value is (this * b)
    public Complex times(Complex b) {
        Complex a = this;
        double real = a.re * b.re - a.im * b.im;
        double imag = a.re * b.im + a.im * b.re;
        return new Complex(real, imag);
    }
}

Both of these examples of encapsulation are, shall we say, "self-contained." They don't rely on any external dependencies to function.

The problem of encapsulating code and data gets a bit more thorny when you start designing business applications. The reason this is true is because business applications concern themselves primarily with collections of entities and the relationships between those entities. While there can and are operations that can be performed atomically on individual entities, this is rare. It is more common to perform operations that affect the relationships between entities or the state or number of entities within a collection. Consequently, most of the business logic is more likely to be found in object aggregates.

To illustrate, consider an ordinary business like Amazon. There's no particular reason to pick Amazon, other than it is unremarkably similar to other businesses in many ways: it has customers, inventory, orders, invoices, payments, credits: the usual suspects.

What can you encapsulate within a Customer entity that can be atomically executed, divorced from other entities? Well, maybe you can change their last name. That's a data change in the database that can happen automatically in a repository somewhere, using an anemic data model. Perhaps you can change their password hash. That requires some logic, but it's unlikely to live in the Customer entity. It's more likely to exist in some security module.

All of the interesting business logic lives outside of the fundamental entities. Consider an Invoice, which is not an individual entity, but rather an aggregate of several entities. What can you do inside an Invoice class, divorced from the rest of the system? Well, you can change the shipping address. That's simply a change to a foreign key in the Invoice entity. You can calculate a Total (the sum of the line item quantities and costs), and finally we get to some non-trivial logic that can be encapsulated in the entity itself. Maybe the line items have a line-item total property on them, so there's a bit of logic there.

But what if you want to calculate a balance? Now you have to go somewhere else besides the Invoice to make that calculation, because the Invoice doesn't know anything about all of the other invoices (by design). That could happen in the Customer entity, but it's just as likely to occur in some Accounting module elsewhere.

And then you have linking entities, entities whose sole purpose is to provide connections between entities at the data level. There's generally no logic in those whatsoever.

So at the bottom of your data hierarchy are simple data transfer objects. When combined into aggregate objects, they become useful from a logic standpoint, and any or all of them are subject to processing by any number of software modules, treated as simply data. When you think about it, it doesn't really make much sense to bake a lot of business logic into something like a Customer object, because now you're tightly binding that object to your specific way of doing business.

Should classes encapsulate data and logic? Of course, when it is appropriate and useful to do so. The core idea in software design is suitability. There are no absolute principles; software design techniques must always be evaluated in the context of your specific system to determine if they are appropriate for your specific functional and non-functional requirements.

7
  • 1
    Thank you for your time and your answer. So I take it that the operations you could do inside domain objects should only affect the data they own (or aggregate) and should only affect/use the data that can be eagerly loaded into memory. But given these restrictions, I would be afraid to put anything in these objects to begin with. If I have a TodoList, it might make sense to consider the TodoList and its Tasks an aggregate, where the TodoList is the root. But what if my TodoList has to support over 100.000 tasks? Commented Mar 19, 2020 at 15:32
  • Unless the TodoList knows how to dynamically load its tasks when needed (and those that are needed), probably via a repository, I all of a sudden need to push the logic outside to my application layer. And the question is, why wouldn't I put it there in the first place? Commented Mar 19, 2020 at 15:34
  • The ability to handle 100,000 entities is the reason we have databases. A user of the system doesn't need to know about all 100,000. You only retrieve those todos that are specific to him. Commented Mar 19, 2020 at 15:42
  • All of them might be specific to him, accumulated through time. Of course, he will never see them all at once on the front-end (he might see them in a paginated table, or only the active todos), but he might have a search function. The search function would have to search for a string inside all 100.000 of them. Should I perform this search on my web server on inside my database? I would say in my database because there I can have full-text indexes. But then what would my aggregate root look like? Commented Mar 19, 2020 at 15:59
  • class TodoList { Task[] tasks; } //This wouldn't make much sense if I lazy-load my tasks when needed and possibly only partially load them (say the first 10, or the results of a search query). Commented Mar 19, 2020 at 16:00
5

What kind of logic can Domain Objects realistically contain?

Think "finite state machine":

Given message a
    When I start in state X
        Then I should end up in state Y
    When I start in state Y
        Then I should end up in state Z

In effect, our domain model uses copies of input data and its own internal logic to compute its new state.


Now, I think you are close to a hard part, but that you haven't quite identified it; specifically, which data gets locked when we are updating the domain object? There's not a lot of real difference between "pass two stale copies of friend lists to the domain object, and let it compute the mutual exclusion", vs "pass a stale copy of the mutual exclusion result to the domain object".

So I could call some database query to ask for a list of all mutual friends, and then when that data is a couple of nanoseconds old pass it to my domain model, and let it decide what to do.

Of course, during those few nanoseconds, and also while the domain model is doing its own work, the data in the database might change in a way that changes the answer to the query.

If that's not good enough, then we need to start thinking about how to make sure those lists are locked against modification while the domain model is doing its thing.

If the things we need to lock are in different databases, then we are going to be vulnerable to various consistency issues.

And notice that in some cases, such a lock simply isn't possible -- there's nothing I can do in my domain model to prevent the weather in Bucharest from changing. It's outside of my control, so I have to be content with a snapshot of information about the past....

And in some cases, where things get big enough and clumsy enough, you have to go back to the subject matter experts and ask "is this constraint under our control? because if it is both expensive and arbitrary, then maybe we should change it." And sometimes you can relax the constraint, and other times you need to spend more money on your architecture to fix it.

Truth be told, to some degree "domain objects" are an illusion we use to make the logic easier to manage. The data structures we maintain in transient memory are just a convenience for calculation; when we have to defend our claims in court, it's the durable representation of that information that matters. In this framing, the real thing that we are doing in the domain model is computing what command to send to update our persistent storage.

2
  • Thank you for you answer, data consistency is a problem that's definitely worth bringing up though not exactly my main concern. Assume my persistence is consistent and whenever I need to assure this, I can open a transaction. Also assume that we are fine with whatever the weather service tells us, regardless of the information being a bit stale in reality. The problem is that even in the context of a transaction, my database and my web server might live on different machines which do not share state. Commented Mar 19, 2020 at 15:48
  • As you outlined, my web app needs some input state in order to compute the new output state. However, most (if not all) of my input state needs to be initially constructed from the database state. Since I cannot pull the entire database in the web app, I need to be mindful and request as little as possible. It is very hard to know in advance exactly how much info I will need. For instance, in my example, if I find that I'm already befriended with the userB, I will not need to pull his list of friends from the database in order to compare it to mine. Commented Mar 19, 2020 at 15:53
4

I'm going to come at this from another angle. The answers so far have been great in a "understanding the essence of a system and it's limitations" kind of way. But I believe I can provide a more concrete answer:

"Model it."

In a way, you are misunderstanding the purpose of a domain model. It is not meant to represent the "things" in a system that interact with one another (that's what a physical model is for). Rather, it is meant to be a useful abstraction of the behavioral requirements of a system.

All we are really talking about here is "which steps of a process" are suitably modeled in your domain. For example (your example), given a User entity with the functional requirements you have delineated above:

  1. The User must not already be your friend
  2. You and the other User must not have common friends
  3. In Bucharest it must be raining

We can in fact derived a model that optimizes for these conditions. What does this model look like?

    class PotentialRelationship {

        int FromUserId;
        int ToUserId;

        bool AreAlreadyFriends;
        bool HaveCommonFriends;

        void Codify(WeatherInBucharest forecast);
    }

You see what I did there? I just re-framed our internal state such that it moves the computation of our values: AreAlreadyFriends and HaveCommonFriends into our database (as opposed to moving the business logic that consumes these values). Now we can simply hydrate our new optimized PotentialRelationship entity with the appropriate values (however they are derived) and keep our invariants in our domain.

I have yet to encounter a problem that cannot be modeled using the approach above. At some point in a business process a decision has to be made or a value has to be mutated given the result of some conditions. Where that result is computed is an implementation detail. If it needs to be done in a database so be it. Just keep the "decision"/"mutating" in your domain.

Said another way, our domain does not need to be given the responsibility of computing the intermediate values that make up a business decision, rather, it should be given the responsibility of mediating the decision itself. After all these decisions are the essence of our functional requirements right? How the inputs are gathered is an implementation detail.

And to be clear. I'm not necessarily recommending the above so much as providing "prior art" on the matter. Of course there are times where it doesn't necessarily make sense to adopt the above, where doing so may be over-engineering (or worse complicating), but it is possible to model this kind of invariant should the business value make doing so prudent.

This answer is meant to exist along side those already provided.

2

The first couple of paragraphs of your question are spot on. All objects should expose only business logic instead of data. This gives the application a huge gain in maintainability, since everything happens locally to the object.

I agree with your logical conclusion, that some objects may need access to database or other external systems to provide useful business logic. This is the only way it would work, and I've been building software this way for almost a decade now. It works.

This is where the confusion is coming from:

People say domain objects must not use repositories

This comes from a completely different perspective. Most projects do not in fact use object-orientation the way you describe it. Most projects are entirely comfortable with anemic "objects". These kinds of, shell we say procedural designs work completely differently, and this restriction might make sense there. It doesn't apply to object designs.

Domain objects don't benefit from Dependency Injection

There is no such rule. Is this some restriction of some framework or library? To be clear, yes, your objects should get dependencies in their constructors just like everybody else. Why wouldn't they?

Fair warning: You are trying to do object-orientation right. Most projects (all I've seen in the last more than 20 years) don't do this. So you are deviating from the de-facto standard.

If you continue on this path, you may find yourself forced to rethink "repositories", your complete design, even question layered architectures.

1

When discussing "domain" layers, one of the most common questions/confusions is between what constitutes domain logic and application logic, as these layers are now separate.
I blame the past development scene for introducing the amalgam called "business logic" that lumped domain and application logic together, and I strongly suspect that developers who were taught about "business logic" struggle to now separate what should actually be domain/application logic.


Skipping ahead a few steps here, my attempt at defining the distinction is this:

Domain logic is a "button" that, when you press it, performs a well-defined actions based on provided parameters. It is used to define specific behaviors of your service.

Importantly, domain logic is built in a clean room. It does not distract itself with second-guessing the input it receives, except if second-guessing is the domain object's sole purpose (e.g. a validator).

Application logic is the orchestration of service behaviors in order to correctly service the request that triggered it. If your domain logic is a button, then the application logic is the operator who knows which buttons to push when and how. This sounds vague, and it somewhat is because the content of the orchestration is highly contextual and can take many different forms.

Importantly, application logic is the shield that cleans the input so that it can be handed off to the domain logic, because the domain logic need to operate in a clean room.


This brings us on to validation, where does it belong?

Well, annoyingly, it's not that simple. Different kinds of validations exist, and they belong in different places. Validating if the user ID value you received is actually a GUID (and not the string "banana"), validating if the user ID refers to an actually existing user, and validating if this user is able to befriend the other user are different responsibilities of different layers.

Therefore, it's not as straightforward to answer this. Some of these validations are so trivial that encapsulating them in an additional validation layer may be one layer too many, or it might be very necessary if this codebase intends to grow in the future. I cannot judge that.

You want to validate early so as to prevent unnecessary work that could have been avoided. Your current example lands somewhere in the middle, it can't be done on the API level as it requires deeper access, but at the same time you don't want to do it in the deeper domain level as it's very likely that this means you've been loading a lot of data without even doing a basic sanity check on the input. You need to balance these considerations.

Without further details on your specific context, I suggest defaulting to defining the validators on the application level, and using them as the "cleaners" before you hand things off to your domain logic. This comes with a few benefits:

  • You can group all your validation rules for one service method/command/..., effectively giving you the ability to define specific validation for specific endpoints, with reusability in mind.
  • The application layer has access to the infrastructure layer, which you're going to need to make use of if your validation is stateful (i.e. comparing to existing state) and not just a pure function.
  • It remains possible for an application-level validator to implement a lower level domain method that performs a domain-specific operation that is needed in order to confirm the validation.
    • For example, if this is a matchmaking application, you might only allow friendships between users if they meet a minimum match "score" based on your proprietary matchmaking calculations. If so, then the actual calculations that yield the score are domain logic, and the actual minimum target is either domain or config logic. However, the actual check if the given score meets the given minimum value is a concern for the application-level validator.

Whether you allow your validator to call your persistence layer, or if you prefer to pre-fetch the data and pass it to the validator, is up to you. I prefer the former, but not everyone does.
It's not a hill I'm going to die on, but my personal opinion here is that an application-level service is allowed to access a persistence-level datastore, then the application-level validator is allowed to access that same persistence-level datastore. The access is regulated across the whole layer, I don't bother making within-layer distinctions on who gets to access what.


Lastly, I'm going to point out briefly that performance is a real world consideration that matters.

Things like the repository pattern make a lot of sense for in-memory collections, or data stores that you can access with a negligible cost.

However, data sizes have grown and, at the same time, data store technology has become significantly better at locally optimizing itself. This leads us to the conclusion that it makes no sense to pull massive lists of data into memory to perform a trivial operation on them.

It's important to consider, however, that application logic patterns tends not to prescribe offloading such work to the data store, because this query logic can be describe to be application logic. We must instead understand that the performance considerations are an additional priority which pushes us away from having the application logic control all of the actual logic.


So let's get to your actual example.

  1. The user must not already be your friend
  2. You and the other user must not have common friends
  3. In Bucharest it must be raining

First, let's look at the persistence layer to optimize for data store performance. Each requirement maps to a very specific data operation.

  1. Persistence client IUserRepository with method: User GetFriend(int userId, int friendId)
    • Returns null if the user does not have this friend.
    • You can return a bool if you prefer. I don't think loading a single entry is meaningfully wasteful so I prefer not having both a "return the object" and a "return if the object exists" method, but this is your call to make.
  2. Persistence client IUserRepository with method: User[] FindUsersWithTheseFriends(int[] friendIds)
    • A "common" friend is a friend who has both of these users in their friend list. Therefore, this method finds any people who have all of the given users in their friend list.
    • Same comment about returning a bool versus returning the actual result. Your call.
  3. Persistence client: IWeatherService with method WeatherInfo GetCurrentWeatherFor(Location l)
    • I'm ignoring the details here and assuming that you have an external API which tells you the weather in a given location.

Next up is the application layer:

  • BefriendValidator
    • Injects a IUserRepository and an IWeatherService
    • Has a Validate(int userId, int friendId) method to perform the validation.
    • During validation, calls the above methods and confirms that the output is what you expect it to be.
  • UserService with a Befriend(int userId, int friendId) method
    • Has access to a BefriendValidator and calls it
    • If not valid, returns negative response.
    • If valid, it instantiates the User(userId) domain object and calls its Befriend(friendId) method.
    • Returns the outcome of the befriending.

Lastly, we haven't discussed the internals of the User domain object, but this is much too broad for me to answer for you. There are myriad possibilities here, depending on what kind of application you are building.

  • Does the friend need to consent to being a friend?
  • Is friendship a one-way or two-way relationship?
  • Does your application calculate a friendship score and subsequently categorizes what kind of friendship this is?
  • Is there any comms sent to either the user or the friend about this?

Your domain object does whatever your application is designed to do, and because the sanity check has already occurred in the application layer, it is able to simply do the work without needing to second-guess the input that it received. If it blows up, then that's a failing on the application layer's behalf of not having sufficiently sanity-checked the input.

10
  • Thank you for the in-depth answer, in the 4 years since I initially posted the question I reached pretty much the same conclusions. However, the main issue is that the outcome of befriending needs to be persisted in the database, notifications might have to be sent and so on. That would ultimately still end up in UserService's Befriend method responsibility, leaving the User's Befriend method with not much purpose. In the real world pure functions with no external service dependencies don't really exist or don't really do anything meaningful. Commented Mar 21 at 12:29
  • So I find that "anemic domain models" (as coined by Martin Fowler) are actually perfectly normal in such applications. That's because the meat of the logic is often reliant on many services and integrations, and the most flexible place to do all these operations is actually in the Application Layer (UserService.Befriend in your example). As you mentioned, some operations may be offloaded to the database for performance reasons, so the ideal case where you'd have the entire object graph you need in-memory so that User.Befriend can do its thing without deps is just not feasible. Commented Mar 21 at 12:42
  • All the last 4 bullet points you listed would probably still need database access or external services. Sometimes you need to do a CRUD operation in multiple steps and use a transaction to ensure consistency. The internals of the User's Befriend method are what interest me, but my current take is that there is actually not much purpose for that method. Commented Mar 21 at 13:01
  • Like what is ultimately the benefit of splitting the logic in 2 layers (domain and app)? The orchestration done by the app layer is usually the meat of the logic (validating, calling a payment service, sending comms, saving data). Is there a case where you'd swap the application logic layer? I think both the domain and application layers represent the core of the app and therefore such a split does not bring any gain. In my projects I just have a "Core" layer which holds (mostly) "anemic" domain models and then services which hold the logic. Commented Mar 21 at 13:18
  • @TedChirvasiu: "leaving the User's Befriend method with not much purpose" A notification about the thing isn't part of the thing, it's secondary. Persistence of the thing isn't part of the thing, it's a subsequent step. Domain logic only cares about modeling its own domain models. Obviously, you want to do more than just set these values in-memory and then not use them, but that is not the domain's concern, and this is by design. Wanting there to be more inside this domain logic is orthogonal to the DDD spirit of keeping the domain logic pure and undistracted.
    – Flater
    Commented Mar 21 at 22:42

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.