In MVC isn’t MVC, which hit the Hacker News front page overnight, Collin Donnell describes how the MVC design pattern that we use today isn’t really what was originally envisioned in 1979 by Tyrgve Reenskaug. This prompted me to think about how this architecture, if tweaked even further, maps pretty well to today’s designs of other kinds of programs, and I want to explore two cases in this post: web services and CLI apps. I know I promised a post on the task queuing system I have written in Rust, but that will have to wait for a couple more days.
To recap, MVC stands for Model-View-Controller and is a software design pattern to structure the implementation of apps that provide a “user interface”. The core idea of this pattern is to separate the presentation of the app from its business logic, and this same idea can be applied to other types of apps as well.
MVC in web services
If we take a look at a web service—not a web app; we are talking about the server side only here—we can map the layers of the MVC pattern as follows:
The View layer is where communication with the world happens. This layer contains the code that receives inbound network requests and writes their responses. This layer is purely in charge of deserializing and validating requests, and serializing responses in whichever protocol the service exposes. The code in this layer must restrict itself to, for example, parsing and emitting JSON, obtaining session details, or converting error types (such as exceptions or
Results) to HTTP status codes.
The Controller layer is where business logic is implemented and is sometimes called the “service layer” or, as I call it, the “driver”. Oftentimes there will be a 1:1 mapping from APIs exposed through the View layer and entry points to the Controller, but not always. This is where all kinds of decisions happen, including authorization checks and coordination of access to the various external resources that the service may need. For example, if various database operations need to be grouped in a transaction and retried on failure, this is where the transaction is created and where the retry logic (policy) lives.
The Model layer is where access to the persisted data happens and is where the high level data types for the application may be defined. This is often an abstraction over a database, but it could also be an abstraction over whichever other dependent system the service needs to talk to. For this reason, this layer may also be referred to as the “data access” layer or, more generally, the “provider” layer because it’s where the service talks to other services that provide data.
I saw this design in practice during my time in the Azure Storage team, where our frontend exposed the same set of storage facilities through a multitude of protocols with high code reuse. I have replicated this same design in my own web services with great results and factored out common code in the III-IV framework, whose name is a direct direct reference to this architecture. See the sample application sources to witness the three layers in action.
But careful: note that just because a piece of code formats data does not make that code belong to the View layer. Imagine, for example, a web service that has an API to format HTML content as a PDF file. In such a service, the Controller layer would be the one fetching the HTML document and transforming it into the PDF (even if the PDF is a “view” of some data). The View layer would be in charge of taking the PDF as a blob from the Controller layer and bundling it in whichever output format the API exposes which, in this particular case, might be emitting the blob verbatim with an
application/pdf content type header.
MVC in CLI apps
If we take a look at a CLI app—without a TUI, because if it has a TUI the original concept of “views” applies quite literally—we can map the layers of the MVC pattern as follows:
The View layer is where interactions with the user happen. This is where the app processes the command line flags and arguments and converts them to internal data structures. This is also where help requests are handled, where input data is gathered if the app is interactive, and where progress reporting and error messages are formatted for display. I already mentioned this back in 2013.
The Controller layer is where the application logic belongs. Notably, this layer must not interact with the console at all:
printfs of any kind do not belong here.
The Model layer is where abstractions around the file system—or whichever other external system the app talks to—may live. For CLI apps wrapping a web service or a database, this is where the calls to those systems happen.
CLI apps vary in complexity, however, so structuring the code in these layers may result in overengineering and not be a reasonable thing to do. That said, it is often possible to organize any kind of app in these layers and it is good to think about how the code could be structured, because this thought process can lead to better design decisions. Take two examples:
Something as “simple” as
ls(1)could be organized into these three layers: it has a complex UI due to the myriad flags it supports; it has a certain high-level logic to coordinate various disk operations such as
stat(2)calls and order the results based on whichever criteria the user requested; and it needs to access the disk via the libc operations already mentioned.
Something as complex as Git could also be organized into these three layers if it’s already not so: the View layer could implement the code to handle the many different commands that the CLI has; the Model layer could implement the core worktree, repository, and index structures and the primitives to manipulate them; and the Controller layer could implement the business logic to do things like commit a change or coordinate with a remote server to synchronize repository contents.
I am of the strong opinion that separating the View-specific code (command line handling) from the rest of the app is a good practice in general, and is critical to allow easier automated testing of the app’s logic. As for the separation of Controller and Model in a CLI app, well, it depends on the size of the codebase really.
Now, in theory, given that MVC is a layered design, the View layer can only interact with the Controller layer, and the Controller layer can only interact with the Model layer. This sounds nice, but in practice, expressing these layers in code becomes tricky as soon as you introduce high-level data types to represent the in-memory state of the program. And I’m a fan of using high-level types to validate program correctness at compile time as much as possible.
Consider this example: your service has a
Person instances are created in the Model layer based on a query to the database. The Controller layer calls into the model layer to obtain
Person instances, and returns those to the View layer. The View layer serializes those objects as JSON to return them as part of an API call. So, the question is: where is the
Person type defined? It needs to be defined under the Model layer because this is the lowest layer that needs the type… but then… the View layer must skip the Controller layer in order to reach for the
Person type definition. Conceptually that’s probably OK, but in code this means adding a build-time dependency between the View and Model layers—a dependency that should not exist.
This is why I have found it useful to extend this layered architecture with two extra “layers”: a Data model layer which provides dumb high-level data types to represent the data the application manages, and a Utilities layer that encapsulates code that’s not specific to the app and that could/should well live in a separate (possibly third-party) library. With these in mind, we end up with the following code structure:
It’s critical to note that the objects in the Data model do not encapsulate data access operations: they are not DAOs in the traditional sense of OOO programming. These types are pure in-memory representations of application data with no logic in them. Obtaining instances of these objects from storage and modifying their persisted representation must all happen via direct calls to database layer operations.
Anyway. I do not like to get hung up with naming and trying to shoehorn designs into strict “patterns” when those do not apply. What I described isn’t really MVC if you are a purist, but it’s nice to see how old concepts from the 70s–80s still make sense today with minor tweaks. What I will say, though, is that every time I’ve sensed this split made sense and cut corners anway because the split seemed to complicate things upfront… I ended up suffering the consequences very quickly. In particular, development agility slows downs as it becomes hard to reason about changes, and unit testing soon becomes impossible.