When writing web applications I place business logic in controllers. This has worked well for my small App Dev team- our applications run reliably, perform well, and the code is easy to maintain. See below for an explanation of my design.

Contrary to my architecture, the advice I find in programming forums usually advocates for placing business logic in models. I'd like to understand why.

Based on your experience building MVC + SOA applications, why would you caution against placing logic in controllers and advocate for placing logic in models? What is your rationale and what is the pro / con tradeoff compared to my design?

I admit my design is influenced by my use of Refit, a type-safe REST proxy for C#. One of the reasons I don't want to place business logic in a model (or entity) is because I define service interfaces and entities in a Contract DLL that is shared by the service (WebAPI) and clients (MVC websites or console applications). I don't want data-persistence details leaked to the client (via Load, Save, Approve, Reject, etc methods on the entity).

Another influence comes from my desire to resolve the contradiction in these familiar design principles:

  • Entities should be persistence-agnostic. That is, they should be unaware of how they're saved to a database or Content Management System (CMS).
  • In MVC, place business logic in the model.

If "business logic" includes SQL or CMS statements, the principles are contradictory. If "business logic" does not include SQL or CMS statements, this implies introducing yet another layer- the Data Transfer Object (DTO). Oh God, not another layer.

Also, I feel inertia from the last popular paradigm is causing Object-Oriented (OO) design (where the object is everything) to creep into our MVC + SOA world. Hence the advice to place business logic in the model (the object).

OK, enough introduction. Here's my MVC + SOA architecture (for a Microsoft stack):

MVC + SOA Design

MVC = Model View Controller, SOA = Service Oriented Architecture

  • A model transfers data and UI (validation rules, picklist choices, etc) between a controller and a view.
  • A view receives a model and renders HTML using Razor syntax.
  • A controller receives a model (via ASP.NET model binding) and runs business logic, either locally (2-tier architecture) or externally (3-tier architecture).
    • If locally, controller communicates directly with a data store(s) (database or file).
    • If externally, controller communicates with an external service(s) using Refit proxy (and the service communicates directly with a data store).
  • Minimize business logic in views and models. A view should be thin, hence the name "Razor".
  • Maximize business logic in controllers.
  • Note the difference between SOA and OO.
    • SOA emphasizes encapsulating logic in service controllers, separate from data.
      • Exposed via HTTP / JSON / Refit interfaces.
      • Models and entities are used for validation and data transfer.
    • OO emphasizes encapsulating logic in objects, near the data.

Business Entities

  • Similar to models, minimize business logic in entities.
  • Separate models from entities.
    • A model transfers data and UI between a controller and a view (see above).
    • A model does not transfer data between a controller and a data store or service.
      • This prevents over-posting (hacker adds additional form fields to override model's default values).
      • This facilitates multi-page, step-by-step forms with different fields required on each page. Define a model for each page. The union of all fields on all models = set of properties on entity.
    • An entity transfers data between a controller and a data store or service.
    • An entity does not contain any UI (validation rules, picklist choices, etc).
  • An entity may not reference a model.
  • A model may reference an entity.
    • Constructor accepts entity parameter, maps entity properties to model properties.
    • ToEntity() method creates entity, maps model properties to entity properties.
  • Dependencies flow in one direction:
    • MVC Website --(depends on)--> Entity
    • If local data store (2-tier architecture), MVC Website --(depends on)--> ORM (such as Dapper or Entity Framework Core)
    • If external data store (3-tier architecture), MVC Website --(depends on)--> Refit Service Proxy

Rationale

  1. Prevention of over-posting attacks and flexibility of UI (present as complex form or split form across multiple pages) independent from entity.
  2. Separation of concerns- models specify validation rules and transfer data to/from UI, entities transfer data to/from service, controllers sequence tasks and enforce business logic.
  3. Change of backing services or data stores do not affect the model or view, only the controller.
  4. The logic in service controllers is reusable- exposed via HTTP endpoints and callable from any client written in any language.
  5. My use of Refit C# proxies in no way prevents AJAX. In fact we do plenty of UI updates via jQuery calls to service controllers, secured by JWT tokens.
  • 3
    Your overall post is asking for too much. Is there a specific, software design-related question you want answered? Please edit your question to limit it to a specific problem with enough detail to identify an adequate answer. Avoid asking multiple distinct questions at once. See this Help Center article for further guidance. – Robert Harvey Nov 22 at 0:57

The Model-View-Controller pattern is a UI pattern that follows a few basic principles:

  1. The View is a User Interface, a thin veneer in front of the Controller. The only logic that should be in the View (i.e. "Code-Behind") is that logic which pertains directly to interacting with the user.

  2. The Controller is a thin veneer in front of the Model. It acts as a switch-yard, shunting requests from the View to the Model for processing. The only logic that should be in the controller is logic that facilitates this communication.

  3. The Model contains everything else about your application, including your Data Access layer (in your case Dapper or Entity Framework Code), your Repository Layer (which includes Entities and CRUD methods), and your Business Logic Layer.

The overall architecture so described looks something like this:

Database --> Data Access Layer --> Repository --> Business Layer --> Controller --> View

The arrows indicate the direction of the dependency knowledge; e.g. the View knows about the Controller, but not vice versa, and so on.

The Business Layer can be thought of as a CRUD/Business Transaction converter. On the left side, it speaks Create, Read, Update and Delete, using entities from the Repository. On the right side, it speaks business operations (things like "Print an Invoice" or "Bill a Customer), using what I would call View Model objects. These View Model objects are what the Controller uses to glue the UI to an underlying data representation.

Note in my example how the Business Layer has been abstracted from the Controller. This allows you to use different controllers with the same Business Layer.

  • Thanks Robert for your feedback. I've edited my question to be more specific- in the title and the yellow callout box. My question is about design intent, so I figured Software Engineering was an appropriate site to ask it... I appreciate your feedback. I like the "speak left, speak right" analogy. However, I see the same flexibility in my design. The controller speaks left re: CRUD to the backing service or data store, and speaks right re: business operations to the user. – Erik Madsen Nov 22 at 5:05

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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