4

I'm working on an application and have encountered two different approaches for organizing business logic. There's a general consensus that application rules should be handled in higher layers, so I don't think those are relevant here.

With Rich Models, state and behavior are bundled together within the entities, which seems to align well with traditional object-oriented design. However, as the system grows, I start to wonder: do these entities end up taking on too much responsibility? How do you handle behavior that needs to be shared across multiple entities? It feels like this could lead to complex inheritance hierarchies or even code duplication in order to keep things cohesive. I’m also concerned that this approach could introduce a lot of boilerplate as abstractions increase.

On the other hand, Anemic Models separate the entities’ state from the business logic, with the logic centralized in specific services. While this approach may seem more "procedural" since the logic isn’t inside the entities, I’ve seen definitions of object-oriented design that emphasize encapsulating both state and behavior together, but I’ve also seen arguments that this isn't always a hard rule. I’m left wondering if this separation could actually help with composing services and reusing logic across different parts of the system.

Additionally, it seems like the Anemic Model + Services approach could make testing easier because the responsibilities are more clearly separated. It also seems to favor composing services and handling batch operations, where the business logic doesn’t need to be scattered across multiple entities.

I’ve also heard the argument that if the service is well-defined and only handles business rules, the model isn’t really "anemic." In that case, the model would be the combination of the entity class and the service class, forming a complete unit of state and behavior. This leaves me even more unsure about the distinction between the two.

Notes

  • When I refer to Rich Models, I’m not talking about the Active Record pattern.

  • When I refer to Anemic Models and Services, I’m not suggesting a Big Ball of Mud, where the services end up accessing and doing everything. At least, I don't think I’m heading in that direction.

So, what are the best cases for using Rich Models versus Anemic Models with services? How can you manage the downsides of each approach as the system grows?

So far I haven't tested any of the approaches, although anemic models seem more correct.

1
  • I've seen multiple projects with anemic models, which were utter mess to work with. I'm so traumatized by the experience that I can't write an objective answer. Usually anemic model is used for serialization and is then reused in all other matters, making the whole domain dependent on serialization and frozen due to backward compatibility concerns.
    – Basilevs
    Commented Oct 3 at 5:25

5 Answers 5

5

The question assumes that the decision between rich/anemic models is made based on context; but in my experience developer opinion (and personal style) is often the driving factor here.

Sure, some scenarios might be more conducive to a given approach than others, but in general these are just two different ways of breaking down a problem into more digestible chunks.

Furthermore, rich/anemic isn't a mutually exclusive binary state. It's a spectrum, with varying degrees of richness/anemia, and (as established) varying opinions on where the line of acceptability is.

That's the overall answer to your overall question. The rest of this answer is responding to specific things you mention to provide a nuanced counterpoint.


However, as the system grows, I start to wonder: do these entities end up taking on too much responsibility?

No one said you're not allowed to break up a rich model once it grows too big. Your question seems to assume that the boundaries of a model, once established, can never be changed regardless of how much it grows internally.

That's not going to be a sustainable strategy precisely for the problem you outline: as the system grows, these entities end up taking on too much responsibility.

How do you handle behavior that needs to be shared across multiple entities?

I refer to this as Abstraction 101: separate out the shared logic into a reusable component of its own.

My description is intentionally vague because there's many ways to skin that proverbial cat, and the correct way to abstract it depends on a lot of context that this question is missing.

It feels like this could lead to complex inheritance hierarchies

You're really just pointing out one of the main challenges of software development: correctly modeling your reusable components. Inheritance, composition, myriad patterns, ... there's a lot of choices here, and the onus is on you to find the one that best matches your specific use case.

... or even code duplication in order to keep things cohesive.

If you've duplicated it (WET), then it's not shared logic (DRY). I would only cynically call that a solution to reusability.

While this approach may seem more "procedural" since the logic isn’t inside the entities

I suspect the word you're looking for here is functional, as in Functional Programming. You can write a volume of books on the topic (and people have).

I’m left wondering if ...
I’ve also heard the argument ...
This leaves me even more unsure ...
So far I haven't tested any of the approaches

Not all who wander are lost, but those who do nothing but wonder are lost.

Quips aside, if you want to pursue this, then the train of thought has to leave the station. There's a lot of conversation on this topic, and you can also just go out and do stuff with either approach and see which you like best to model your domain. Sitting around waiting for others to provide a fitting answer to your musings is not the most productive approach here.

2
  • Doc Brown: That was not a typo. "Not all who wander are lost" is a very specific quote. Then using "wonder" in the second half of my quip is an intentional play on words, subverting the first half.
    – Flater
    Commented Oct 2 at 1:49
  • 1
    Sorry, too subtile for a non-native speaker like me :-)
    – Doc Brown
    Commented Oct 2 at 4:21
0

Composing services is one too many responsibilities for the component/layer/tier called/labeled service. A service encapsulates business logic related to an entity (just the behaviour without the state that is encapsulated by the entity), to compose business logic from different services there need to be an extra level of indirection (fundamental theorem of software engineering).

What may seem "procedural" could be reimplemented using polymorphism by implementing behavioural design patterns (command or strategy are most common).

Entities with behaviour, when concerns are properly separated, could grow in number. Entities growing in responsibility is a cue the concerns are poorly separated.

although anemic models seem more correct

By what definition? You need a definition to evaluate correctness.

How can you manage the downsides of each approach as the system grows?

By continuous refactoring to keep the concerns separated and in the initial phase designing the indirection levels so to define a resilient architecture.

-1

The name "anemic model" originates from a proposed anti-pattern of having most or all business logic in services even when large parts of it could be seen as responsibilities of entities or value objects.

Having a simple model might just indicate that the application is simple. There's nothing wrong with that. Having a needlessly thick service layer is what is not recommended.

About services from Domain Driven Design by Eric Evans:

When a significant process or transformation in the domain is not a natural responsibility of an entity or value object, add an operation to the model as a standalone interface declared as service. [...] Make the service stateless.

(Note that Evans considers services as part of the model.)

So, if you need to "handle behavior that needs to be shared across multiple entities", then you're adviced to create a service. Even by Eric Evans himself.

What you should pay attention to is, I think, is the size and complexity of the service layer. When you have the option of simplifying the service layer by having a task handled by an entity or a value object (or class) itself, you should at least consider it.

-1

I suggest you try writing the same section of code both ways.

At first it looks fine, just which class do you want your method on

program_ADM
{
    var service = new service(dep1,dep2)
    var o = new obj()
    var result = service.Calc(o)
}

vs

program_OOP
{
    var o = new obj(dep1,dep2)
    var result = o.Calc()
}

The difference starts to show itself when you have more than one operation

program_ADM
{
    var serviceA = new service(dep1,dep2)
    var serviceB = new service(dep3,dep4)
    var o = new obj()
    var resultA = serviceA.Calc(o)
    var resultB = serviceB.Calc(o)
}

vs

program_OOP
{
    var o = new obj(dep1,dep2, dep3, dep4)
    var resultA = o.CalcA()
    var resultB = o.CalcB()
}

You can see that if I have multiple unrelated operations to perform on the same data, OOP is going to get up with huge constructors with a million dependencies.

If I have a program which is only interested in resultA, I still need to create and inject the dependencies for resultB.

The OOP approach shines when you have multiple, related, state mutating, operations you want to conditionally perform on the same, persisted in memory object. ie a user interface

program_OOP
{
    var o = new obj(dep1,dep2)
    OnClickA += o.ChangeA()
    OnClickB += o.ChangeB()
    OnClickC += o.Print()

    while(running) { drawUI(); }
}

Now you can pass o around the app mutating it as required without having to dig out the service you want from some a global var etc

The ADM approach shines when you have a multiple unrelated calculations to perform on collections of the objects. ie microservices/web apis

program_ADM_MS1
{
    var serviceA = new service(dep1,dep2)
    var serviceB = new service(dep3,dep4)

    bind("/CalcA", o => return serviceA.Calc(o));
    bind("/CalcB", o => return serviceB.Calc(o));
}

program_ADM_MS2
{
    var serviceC = new service(dep5,dep6)
    var serviceD = new service(dep7,dep8)

    bind("/CalcC", o => return serviceC.Calc(o));
    bind("/CalcD", o => return serviceD.Calc(o));
}

program_ADM_MS3
{
    var serviceE = new service(dep9,dep0)
    foreach(var o in repo.AllObjs())
    {
       serviceE.DoThing()
    }
}

I should add a short note a couple of bad solutions to the dichotomy

The "Bounded Context", where instead of services we make a version of the Object for each operation.

 ObjectForA(dep1, dep2).CalcA()
 ObjectForB(dep3, dep4).CalcB()
 ObjectForC(dep5, dep6).CalcC()

This has a number of downsides. For one the naming gets ugly, two deciding which object gets which method can be hard, you end up with inheritance problems when you want objects to share common methods.

Another solution is to pass in different versions of a dependency depending on the operation you want

 var o = new obj(depA)
 var result = o.Calc() // returns A

 var o = new obj(depB)
 var result = o.Calc() // returns B

Very "Ask don't tell". However this falls into what I call the init() trap. Where an instance is created to work in a particular way, but can't be reused for a different purpose.

2
  • 1
    I feel the "when you have more than one operation" is an unfair comparison. If A and B are that distinct that you'd create separate services for them, you're probably not going to want to start piling them into the same domain model (as a matter of OCP adherence). If A and B are closely related enough that it'd be reasonable for them to be added to the same domain model, then you'd also expect it to be reasonable to add them to the same service.
    – Flater
    Commented Oct 3 at 2:34
  • Not really, For example order.Buy() order.Dispatch(). both use the same data set, but the operations are too different in terms of their dependencies and user scope to fit on the same class. If you want to use "different" domain models, you end up with abstract Order with all the properties, OrderForBuying : Order and OrderForDispatching : Order
    – Ewan
    Commented Oct 3 at 16:59
-3

I’ve seen definitions of object-oriented design that emphasize encapsulating both state and behavior together.

That is pretty much the common definition of OOD that I've heard - it's certainly hard to imagine an OO design which does not have these things together.

Then again, state and behaviour together is also the definition of a "program". It's also the definition of a "service".

Perhaps the problem here is that the idea that a computer should both store data and have routines for processing it somehow, is simply a truism of the entire field of computing, and of designing software for computers. There is no computer that stores data but does not process it, nor is there a (digital) computer that processes data but does not store data (at least transiently at runtime). So there isn't ever going to be any useful piece of software, howsoever designed, that does not consist of both state and behaviour, and does not have this design motif somewhere.

It's usually taken for granted in OOD (though not always said explicitly) that an "object" should correspond to a "class" in an OO language, which assembles simple fields and methods together, and that a single (and non-trivial) OO program consists internally of many of these objects together in an orchestrated interaction.

It's this criteria about the scale at which the object pattern emerges, that really distinguishes the OOD style from just any old software design philosophy.

I really don't see how an "anemic model" is not simply a rejection of OOD principles, if it implies that classes consisting of both fields and methods together are no longer the prevalent design pattern internally within the program, but instead carrier structures are being defined for data, and algorithms are being defined separately which operate upon structured data but do not accompany the data in transit around the program.

So the question is really a disguised form of "when should I use OOD and when should I reject it?".

When to use OOD?

My advice frankly is that I really have never seen two programs side by side, one written with OOD and one not, both by competent people in their own style, and the OOD version was simpler, clearer, or faster.

This is defining OOD as the shunning of free methods and simple data structures, and proceeding immediately to assemble data and methods together in classes.

At a certain level of complexity, even procedural styles start to adopt modules and internal interfaces at a high level, which thus start to resemble objects in the sense that structures and algorithms end up in an assemblage, but the hallmark of OOD is the use of objects pre-emptively and at the lowest possible level (not at the highest level as found necessary), and the correspondence in scale between an object and a class.

I can't speak from experience in every kind of software, but when talking about designing software for business data processing applications, my advice would be to avoid OOD.

You will find relational database engines which sit at the centre of business data processing do not proceed from the OOD philosophy.

Every attempt of OO fanatics - often amongst much noise and complaint - to replace relational databases have failed through their own lack of foresight and understanding about the complex, well-integrated, and time-honed functions which relational database engines perform.

What then remains is an "impedance mismatch", where those who wish to continue using OOD and OO languages for application code outside the database, find that their concepts are irreconcilable and functionality mismatched with what relational database engines provide. For those who hold on to OOD in this context, gratuitously complex translation layers then ensue, which can be avoided by giving up the OOD diktats.

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