Software Engineering DISCUSSION

Should business logic live in the model classes or in a separate service layer?

Started by universityhelponline domain modelservice layeranemic domain modelbusiness logic placementsoftware architecture
5 replies 248 views 6 participants
Latest activity · 30 Sep 2026

Should business logic live in the model classes or in a separate service layer?

universityhelponline Software Engineering Forum
#1

In our order management application the Order class has only fields with getters and setters, and an OrderService of about 1500 lines does everything else: calculates totals, checks whether a status change is allowed, calls the payment provider and sends emails. A senior colleague says logic belongs in services and models should stay plain. A book I am reading calls exactly this an anemic domain model and an anti-pattern.

Which is right, and is there a practical rule for deciding where a given piece of logic should go?

Community replies 5

Re: Should business logic live in the model classes or in a separate service layer?

#2

Both positions are right about different kinds of logic, so the rule is to separate the two. Domain rules are statements about the data itself that must always hold: an order's total is the sum of its lines, a cancelled order cannot be shipped, a quantity is positive. Application logic is the workflow around a use case: load the order, ask the payment provider, save, send a confirmation.

Domain rules belong on the object whose data they protect, as methods like order.addLine(product, qty) and order.cancel(reason). Workflow belongs in a service. The anemic model criticism is about putting the first kind into services as well.

Re: Should business logic live in the model classes or in a separate service layer?

#3

The reason to keep rules with the data is that it gives each rule exactly one home. While status has a public setter, every piece of code that changes it must remember the allowed transitions, and sooner or later one does not. If the only way to change status is order.ship(), which refuses unless the order is paid, an invalid order cannot be constructed from anywhere in the program.

Such methods are also the cheapest code to test: plain objects in memory, no database and no mocks. A test creates an order, adds two lines at 10.00 and 5.50, and checks the total is 15.50 in a few milliseconds.

Re: Should business logic live in the model classes or in a separate service layer?

#4

What should stay out of the model is anything that talks to the outside world: database queries, web requests to the payment provider, sending email, reading the clock or configuration. An entity that does those things cannot be tested or reused on its own, and its behaviour starts depending on infrastructure.

Logic that spans several objects and belongs to none of them, such as pricing that needs an order, a customer's discount agreement and a tax table, fits a small stateless domain service that takes those objects as arguments and does pure computation. The application service then remains a thin coordinator that owns the transaction.

Re: Should business logic live in the model classes or in a separate service layer?

#5

Your colleague's approach is a reasonable one in its proper territory. For an application that is mostly forms over tables, with few rules, plain data classes plus short service methods are simpler and quicker than a rich model, and there is nothing wrong with that.

The warning signs that you have outgrown it are the ones you describe: a service of 1500 lines, the same validity check repeated in several methods, and bugs where one code path forgot a rule that another enforces. At that point the rules exist; they are simply scattered.

Re: Should business logic live in the model classes or in a separate service layer?

#6

You can move towards a richer model gradually. Pick one rule that is duplicated, the total calculation is a good first choice, move it into Order as a method, make the service call it, and remove the setter that allowed the total to be written from outside. Repeat with the status transitions. Each step is small and can be covered by tests before and after.

A good end state is a service method that reads like a recipe: order = orders.get(id); order.cancel(reason); orders.save(order); notifier.orderCancelled(order);. Every decision is inside the domain object, and the service only sequences the steps.

TEP COMMUNITY