Logic is done within services and where the complex dependency graphs live. Repositories do the data retrieval. The controller is a traffic cop. The model is a data transfer object with maybe some calculated fields. The view makes things pretty.
Logic is done within services and where the complex dependency graphs live. Repositories do the data retrieval. The controller is a traffic cop. The model is a data transfer object with maybe some calculated fields. The view makes things pretty.
The problem is that the models often get too tied with the database implementation. Then, you're separating business logic that doesn't make sense to separate.
The fat model ruins single responsibility because the database table is not a good proxy for one reason to change.
“You're very clever, young man, very clever," said the old lady. "But it's turtles all the way down!"
But also there's no reason you can't have non-database-backed classes sat next to AR models, if they make sense in the domain. Not everything needs to be a database row.
None of this says that putting business logic into a separate class to the thing that happens to back onto a database row is correct because of single responsibility. That's a fundamental misunderstanding of the SRP.
In HTML, in desktop GUIs/mobile Apps, in Databases, maybe games.
HTML is founded on the principle that your data is semantic, it knows how to submit and modify itself. You have <form> tags. Every thing you can do in your view, is another thing you don't have to submit to some slow service or wait for a controller to do.
GUI frameworks are often like this as well, complex well-written UI components tend to remove a lot of supporting code from Controllers that just was there to half-support the UI having a slightly different formatting. The more you can push into your view, the more you can delete controller code, the more model code becomes a glorified "save" button. If your data is well-formed, and/or semantic, this model code can be very simple.
Games can be a lot like this as well - after you've loaded your geometry, pushing everything off to the GPU means your "game" code can be focused on simple things. Advanced things like physics and collision can be handled by actor-event logic. Controllers are still there to do some puppeteer-ing and models exist so save and networking code works.
Even databases can be view-heavy. A lot of database work amounts to building views of the same set of tables. Some analysis is done on the views, maybe, but keeping everything in normalized tables means losing out on view caching. Possibly this is more how vector databases work, as part of the data is where the data is not just what type the data is or which table it happens to be in.
Of course, none of this works great for heavy-networking cases (lots of net-code, lots of multiplayer events, lots of cross-database replication), but those are maybe less interesting to me - I prefer decentralized distributed models anyways. I generally value having more functionality at the local compute node, versus some monolithic service that may fail in a couple years time on the other side of the planet.
But that's just me.
Now the Models become obese. So you split out the Repository logic.
Now you need a place to do "client onboarding" which creates a Customer, a User, a BillingInfo, a bunch of things to show the new client a non-empty environment... Where does this live? In the Customer model? Nah. In a repository? Certainly not.
No. That's when the OnboardingService comes along.
note: We use jOOQ so our "Model entities" are generated from the db schema and thus logic-less.
Where would you put validation logic, though? I mean, some of it might need to just check whether a domain object has its fields filled out, but other validations might need to cross reference DB data to make sure that everything is valid in accordance to the business rules.
Ergo, we might have Model-View-Controller-Service-Repository-Validator
Even without being silly, it's interesting to see how different languages and frameworks handle common concerns, for example, with some you will have repositories, with other the model objects themselves will handle queries through some ORM.
While it certainly isn't always possible, I've definitely seen it be aggregated in a single place in monolithic codebases that focused on CRUD.
Essentially, it was structured like so:
+--------------+ +------------------------+ +-------------+
|Resource (API)|---|Service (business logic)|---|DB Repository|
+--------------+ +------------------------- +-------------+
| \
+-----------------------+ \ +-----------------+
|Validator (validations)| \|REST clients etc.|
+-----------------------+ +-----------------+
The Resources were typically just API endpoints (HTTP), that passed data onwards to Services. Those could then call upon other services or service requests on their own: store/persist data with Repositories, deal with scheduled processes, or pass data on to other systems through REST clients etc.Before Services did any of those, they reached out to a Validator to make sure that the data matches the business rules and constraints. Sometimes there was additional validation context passed in (essentially a map and some enums), sometimes there were certain constraints that an entity needed to always follow within the context of the business and so on.
In practice, it was good because you could see all of the constraints in one place, that an entity has to match before it can be persisted in the DB, or passed onwards to another system. But then again, that's not always viable in more modern architectures. Of course, it wasn't the most traditional approach to MVC either, even if those concepts translate pretty well to it.
Basically, like sibling post mentioned: you put it where it makes sense.
Doing validation on db entities, is certainly not a place it makes sense (except in the most trivial cases, hence the common mistake).