We really need to call it Model-View-Controller-Service-Repository and be done with it. That is actually what happens 99% of the time.
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.
No, please for the love of god stop writing services. Having random grab-bags of methods makes it infinitely harder to find what code lives where, instead of putting it into models where we have 20+ years of OO design principles (single responsibility, open to extension, liskov substitution, interface segregation, etc) to guide us. "Fat models, skinny controllers" is a Rails guiding principle for a reason
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.
that's fine! I'm happy with heavier views. The thing i'm arguing against is the Rails pattern of having FooBarService classes that are effectively just a single method and cobbled together in completely random ways instead of using actual model methods.
Until the moment that models have to call other models to get anything done.
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.
This is a specific problem with the Active Record pattern, rather than something intrinsic to MVC, and it's exactly why AR is fine for simple apps where the business logic looks a lot like manipulating a row in a database, but breaks down for anything more involved.
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.
Eh? It's a service. It represents a domain operation (or group of operations) that hasn't been coalesced into either a value type or a transient entity, both of which are also models without database tables, both of which are preferable to a service if they fit the domain.
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.
I too use services and they are where the business logic lives. In the case of “create user” they’re basically no-op repository methods, in the case of “sync user profile data” they may invoke storage or external service calls in a certain business-logical order, but they map exactly to the applicable domain(s) - putting user-profile methods in the user service is when you end up with muddled service boundaries
You start with MVC, following "the fat-model, skinny controller" rule of thumb.
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.
Fat models is the opposite of single responsibility: doing both business logic and data retrieval/storage logic inside the same class is already more 1.
Lol, but MVCSR (MVCaeser) sounds like an architectural dictatorship where one must follow all rules or else be outcast from citizenship. Where some executive will stab you in the back and sell it to a private equity firm to be cut and squeezed by all the lands and your intentions and beautiful code structure is murdered by junior bootcamp devs. /s
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.
Validation logic needs to be spread throughout the system because your validations are all context sensitive to the operations being performed. It's not always possible to front-load that into a simple schema in your transports and each component needs to be correct in its own domain.
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.
Most MVC frameworks have some soft or form DTO notion, as a form does not map 1-on-1 to one db record. The form DTOs in our app also holds the logic to validate itself.
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).
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.