Skip to content

Minimal APIs, CQRS, DDD… Or Just Use Controllers?

Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


You’ve probably seen this meme floating around, and it’s funny. Why? Because there’s some truth to it. At one end, we just have MVC controllers. At the very opposite end, simply MVC controllers again. But there in the middle lies all the abstraction, libraries, tools, etc. The list goes on.

YouTube

Check out my YouTube channel, where I post all kinds of content on Software Architecture & Design, including this video showing everything in this post.

So, who’s right? Well, like most things in software, it depends. But the answer “it depends” is ridiculous unless you say it depends on… which I’m going to do.

I’m going to take a really simple example that has some abstractions in it, and I’m going to take that to something purely concrete. Along the way, I’m going to explain the trade-offs. This is less about being a dumb or smart developer. The middle isn’t necessarily even bad. It’s about design choices and trade-offs. This isn’t to say that one is better than the other. It’s to show that your decisions affect things like testing, extensibility, coupling, and, as always, that context matters because context is king.

I’m going to jump into my code example, but first I’d like to thank Current for sponsoring this video. Current’s an event-native data platform that feeds real-time business-critical data with historical context and fine-grain streams from origination to destination, enhancing data analytics and AI outcomes. For more on Current, check out the link in the description.

Starting Point

Here’s a pretty simple example. We have an OrderController, and what we’re really going to look at and rejig a little bit here is this MyOrders method.

So this particular route, right now what it’s doing is using MediatR to make a request to GetMyOrders, and what it’s doing is passing in the identity name of the user that’s logged into the system.

If we look at that request, the GetMyOrders request for MediatR and the handler for that request.

First, it’s injecting an IReadRepository. It’s using that by also specifying a specification. The specification is basically adding some where clause and other LINQ behavior to Entity Framework Core, the ORM that we’re using. But that’s all backed by this repository. So we have the specification that’s doing the filtering. We’re getting our list of orders out, and then we’re doing some transformation of that into this OrderViewModel.

So right now we’re using MVC, MediatR, a specification, a repository, and Entity Framework Core.

Let’s start ripping this apart and replacing some of the abstractions with concrete implementations, and then talk about the trade-offs.

Removing the Specification and Repository

First, I’m going to remove the specification and repository entirely.

I’ll just get rid of those and use the DbContext directly. I’m replacing this with the actual query needed. I’m doing exactly the same thing. We end up with the same result.

So what are the implications of doing this? What’s the value of that specification? What’s its utility? What do we lose by removing it?

Its value and purpose were to capture that precondition—the filtering of our username when passing it to the repository—and it was also doing the eager loading of line items so you don’t accidentally forget to do that. That was its purpose in this context. Again, context is king.

How many usages did it have? One. Is it worth creating that indirection for one usage? In my opinion, absolutely not. But again, this is a sample to illustrate things. If you had this used in 20 different places and always needed that same filter, yes, it’s worth capturing that explicitly and giving it a valuable, meaningful name for that use case.

With the repository, because it was using the specification, we kind of had to use both together. Otherwise, we’d get too much data and filter stuff in memory. That makes no sense. So they went hand in hand.

Is there value in the specification and repository? Absolutely. It depends on what you’re capturing and how many usages you have.

You might say, “I hate this. I don’t like this at all. I want to abstract my use of my ORM because I may change data access. I may change my ORM.” Sure. Same question: how many usages do you have of the ORM for orders? Do you have a thousand or do you have ten?

If you need to change ten usages of your ORM, is an abstraction valuable? In my opinion, no. Change the ten usages. Do you have a thousand usages? Then maybe it’s worth it.

I’d also pose the question: why do you have a thousand usages? Could be entirely valid. But this all comes down to your degree of coupling and also testing.

You may say, “That repository and specification were easier to test than using EF directly.” True. Maybe not. I don’t agree that this isn’t testable. To me it is equally testable to fake out this particular data set just as it would be to fake out that repository.

So the abstractions you’re creating or using—do they have value?

Let’s keep dismantling because this next part brings up another great trade-off.

Removing MediatR

Let’s get rid of MediatR. Do we need it? What’s the value? What are the trade-offs?

I’m going to take all the contents of the handler and put them right inline in the controller.

When I do that, immediately something should stand out: we don’t have the username. Where was the username coming from? MVC. This type is only accessible in the controller. We were using that to pass into our request. Now we have to have it directly inside our application code.

Now we’re muddling the water between application code and framework code. MVC is about HTTP. Now we have application code that is really tied to the web framework. Before we had an application request. Now we really have a web request.

We’re coupling directly from MVC into our application code. Does that matter?

Does it matter that you’re building a web app that returns HTML or JSON? It’s a web app. It’s built on top of HTTP. There are no other entry points. Or do you need other entry points?

What I mean is this: MVC could be one entry point. Minimal APIs could be one entry point. But you might have another. For example, if you’re using something like a web + queue worker pattern, your MVC could place messages on a queue, and separately you have a worker executing tasks. Same codebase. Could be deployed as two separate units or together. But two different entry points.

If that’s the case, you don’t want your application code directly in an MVC controller.

So the problem here is you’re coupling your application logic to ASP.NET Core MVC. Is that a problem? No. If all you ever need is HTTP and that’s what your app is, that’s totally fine.

Could you still use something like MediatR when you have multiple routes calling the same command or query? Yes. That’s an applicable use case.

Controllers aren’t the problem. Minimal APIs with CQS, AutoMapper, MediatR, FastEndpoints, vertical slices, DDD, all this stuff, none of that is the problem.

The problem is not understanding the degree of coupling you have to the tooling you’re using in the given context and whether it has value.

What I’m Actually Trying to Convey

A few points:

  • Frameworks (MVC, Minimal APIs, etc) are entry points. They aren’t architecture.
  • Your architecture is composed of many different architectural styles.
  • Indirection adds flexibility, but it also adds complexity.
  • Direct coupling isn’t inherently bad.
  • You don’t need an abstraction for everything unless something is actually limiting you.
  • Use abstractions when they solve real problems, not just because everyone says to abstract everything.
  • Design around your application’s needs, not someone else’s patterns.

Join CodeOpinon!
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.