Skip to content

Is DDD Overkill for My CRUD Project?

We hear it almost every week. A developer leans back, half-smiles, and says something like: "Domain-Driven Design? Sure, for a big enterprise system. But mine is just a simple CRUD app. Wouldn't DDD be total overkill?" The tone is friendly, sometimes even a little apologetic, as if they're letting us down gently. And the reasoning sounds airtight. CRUD is simple. Domain-Driven Design is heavy. Why bring a freight train to move a couch?

We get the instinct. Most of us have rolled our eyes at a slide deck full of Bounded Contexts for an app that, frankly, has one table and three buttons. But once you scratch the surface of this question, two different things turn out to be hiding under the same word. One of them really is overkill for your CRUD app. The other one isn't even optional, and you're either doing it well or doing it badly right now, whether you call it DDD or not.

There's No Such Thing as a CRUD App

We're going to make a claim that sounds combative, and then we're going to back it up with a Todo list. There is no application that is just data in, data out. Not a single one we've ever seen, including the kinds of apps people use as the canonical example of pure CRUD.

Take a Todo list. It's the cliché. You create a todo, you read it, you update it, you delete it. Four verbs. What could be simpler? Well, look at what those four verbs are actually hiding. When you "delete" a todo, are you doing the same thing every time? Of course not. Sometimes you delete a todo because you finished it, and you want it gone from the list. Sometimes you delete it because the meeting was cancelled, and the task no longer applies. Those two events have the same shape on the screen, but they mean entirely different things in your life. A Completed event and a Discarded event are not the same outcome, and pretending they are throws away meaning that was right there in front of you. We've spent a whole post on why soft delete is a workaround for exactly this kind of confusion.

The same is true for "update." You don't update a todo. You adjust its description because you realized you wrote it ambiguously. You postpone the due date because something else came up. You advance the date because the deadline got moved. Adjusted, Postponed, and Advanced are three completely different events with three completely different reasons, and your "update" verb erased the difference. The data after the update doesn't tell you what happened. It only tells you what is now true.

This is the part most people miss when they call something CRUD. CRUD isn't a description of the app. CRUD is a description of what your storage layer demands of the app. It's not that the domain has four operations. It's that you funneled every operation in the domain into four generic boxes because that's what the database understands. The semantics were there. You traded them for table rows.

If a Todo list has this much hidden semantics, what about the app you build at work? The order system, the booking platform, the internal HR tool, the CMS, the support ticketing thing your team built two years ago. Every one of them has registrations, cancellations, escalations, reassignments, splits, merges, deferrals, approvals, rejections, retractions, reopenings. None of those are CRUD. They were made to look like CRUD because that's what the database understands. The domain didn't agree.

"DDD Is Overkill" Talks Past the Real Question

Now we can get to the part where the question itself goes off the rails. Domain-Driven Design isn't one thing. It's three. And when people debate whether DDD is overkill, they almost always mean only one of those three. The other two get smuggled along for the ride, sometimes as an unfair advantage and sometimes as collateral damage. It's worth pulling them apart.

The first kind of DDD is strategic. Bounded Contexts, Context Maps, anti-corruption layers, the choreography of teams across a large organization. These are the tools you reach for when the org is big enough that two teams are using the same word to mean different things, and a third team is being quietly ground up at the boundary. For most projects we see, this part of DDD really is overkill at the start. It pays off later, but later is later. You don't need to do strategic DDD on day one.

The second kind is tactical. Aggregates, Repositories, Factories, Specifications, Domain Services, Value Objects. This is the part with the ceremony, the inheritance trees, the architectural diagrams that mix UML and flowcharts until nobody quite knows what they're looking at. This is also the part that has real costs. It can absolutely be overkill on a small project, especially if you treat it as a checklist rather than as a set of tools you reach for when you need them. The critique here has merit, and we'd be doing nobody a favor by pretending otherwise. We've written about how your aggregate is not a table partly as an attempt to keep tactical DDD honest with itself.

The third kind is Ubiquitous Language. The idea that the words you use for things in your code should be the same words your domain experts use for them. That a method should be called registerUser, not createUser, because your business doesn't create users, it registers them. That a customer isn't deleted, they're deactivated, or offboarded, or cancelled, depending on which of those things actually happened. This part of DDD is almost free, and it's the part the overkill argument almost never engages with. We've explored what it means to keep that language consistent across people and cultures in Ubiquitous, But in Which Language?.

When somebody says "DDD is overkill for my CRUD app," they almost always mean the tactical part. Sometimes they mean the strategic part. They almost never mean the language part, because the language part isn't visible to argue against. It just sits there, quietly making the code better, or quietly making the code worse. The conversation worth having is about that third part, because that's where the gap between the people who took the question seriously and the people who didn't actually shows up.

Names Are Free Today, Expensive Tomorrow

Here's the part that, in our experience, settles the argument. You have to name your methods anyway. You have to name your tables, your fields, your endpoints, your events, your variables. There is no version of writing software where naming is optional. You're going to spend the keystrokes either way. The only question is whether the names you spend them on mean something.

Compare two trajectories. In the first one, you call the method createUser. It's generic. It accepts a body. It writes to a table. Six months later, the business introduces a concept called invited users, where you can be in the system without yet having an active account. A few months after that, self-registered users become a distinct flow with a different verification path. A year in, reactivated users turn out to need their own handling, because the legal team wants different audit trails for them. You now have one method called createUser that does five things, and a codebase that doesn't tell you which thing is happening when. Your tests are full of conditionals. Your logs are useless. The pull request that finally splits the method into inviteUser, registerUser, and reactivateUser is three weeks of work, including the integrations with five downstream systems that learned to depend on the original generic shape.

In the second trajectory, you called the right method the first time. registerUser. You didn't know there'd be inviteUser and reactivateUser later. You didn't need to. You just named the thing for what it was on day one. When inviteUser showed up, you added it as a new method, alongside the existing one. No refactoring. No three-week PR. No five-system integration story. The cost of naming things well today is zero. The cost of not naming them well, paid later, is enormous.

This is the asymmetry that makes the whole overkill argument collapse on inspection. Strategic DDD is a real investment with real costs. Tactical DDD has real costs too. But getting your language right isn't an investment. It's hygiene. You don't decide "I'll invest in good naming when the project grows." You name the thing you're naming, and you name it well, because the alternative is to name it badly and pay for it forever.

But What About YAGNI?

There's a sharper version of the overkill objection, and it deserves a sharper response. You Aren't Gonna Need It is real wisdom. Not every project grows. Some stay small. Some die. Front-loading complexity has killed more codebases than under-engineering ever did. We don't want to wave YAGNI away.

So let's be precise. YAGNI applies to features and abstractions you might never use. It doesn't apply to thinking clearly about what you're doing right now. A registerUser method isn't a speculative feature. It isn't an abstraction. It's the same method you were going to write anyway, named better. That cost zero. The complexity YAGNI warns against is behavioral complexity, things the code does that aren't needed yet, code paths that exist for hypothetical futures. Good naming has no behavior. It doesn't add code paths. It doesn't add abstractions. It just refuses to discard meaning you already have.

There's another piece to this. Anyone who's shipped software for more than a few years knows that requirements one year from now will be different from requirements today. You don't know how they'll be different. You can't know. But you can know with near-certainty that they will. When that day arrives, your most valuable asset is not your test suite, and not your documentation. It's your past understanding of the domain, encoded into the code itself. A registerUser method tells your future self that registration is a thing and has a shape. A createUser method tells your future self nothing.

The Same Logic Applies to Event Sourcing

We'd be sneaking something past you if we didn't say this part out loud. The asymmetry that makes the language argument so strong, cheap now, expensive later, is the same asymmetry that drives a lot of our thinking about Event Sourcing. It's worth pulling on the thread, even if it's a slight detour.

When you store data as events, you keep the why alongside the what. A BookBorrowed event carries the fact that the book left the shelf because someone borrowed it, not just that the shelf inventory dropped by one. A MembershipCancelled event carries the fact that the customer terminated the relationship, not just that the customer row got a status field flipped. If, a year from now, you decide you don't need that granularity, you can fold it down. Projections discard whatever they like. It is trivially easy to throw away meaning you have. It is impossible to invent meaning you didn't capture.

We won't pretend Event Sourcing is free. It isn't. Projections cost work. Versioning takes thought, and we've written a whole post on how to do it without breaking things in Versioning Events Without Breaking Everything. Eventual consistency is a real model with real implications. There are projects where these costs outweigh the benefits, and we've been honest about that in Event Sourcing Is Not for Everyone. We're not handing out party hats.

But the asymmetry holds regardless of those costs. A year into a project where you didn't capture the events, you can't go back and capture them. The conversations that happened, the decisions that were made, the workflows that unfolded, all of it is gone, smoothed into whatever the latest row says today. If you didn't need those facts, fine, you saved yourself some work. If you did need them, you cannot get them back. That asymmetry is the reason we keep coming back to Event Sourcing as a sensible default for systems where the domain is interesting, not as a hammer for every nail.

Stop Asking If DDD Is Overkill

The question "is DDD overkill for my CRUD app?" is the wrong question, for two reasons. The first is that your app isn't CRUD. It looks like CRUD because the database looks like CRUD. The actual domain underneath is richer than that, and the only question is whether you're acknowledging the richness or papering over it. The second is that DDD isn't one thing. Some parts of it are heavy. Some parts of it are essentially free. Treating it as a single yes/no decision means you're either inviting in machinery you don't need, or shutting out hygiene you can't afford to be without.

Here's the better question, the one that actually points somewhere useful. Which parts of DDD have what value at what price? Strategic DDD: real cost, real value, worth doing later when the org is large enough to need it. Tactical DDD: variable cost, variable value, reach for the tools when the problem reaches for them. Ubiquitous Language: almost no cost, enormous value, start today. If you do nothing else from this post, do that.

And once the language is right, the rest of DDD stops looking like a freight train. It starts looking like what it actually is: a set of tools to help you keep the meaning of the domain alive in the code of the domain. Some of those tools you'll never need. Some you'll need eventually. One of them you needed yesterday. That one is free.

If you'd like to see what it looks like to capture the language of a domain in something more durable than naming conventions, head over to esdm.io and walk through a small example. ESDM is the modeling language we built for exactly this problem, and it's designed to make Ubiquitous Language a file your code can see, not a folklore your team has to remember.

And if you'd like to talk to us about how this question lands in your own team, or about anything else in the world of Event Sourcing, CQRS, and Domain-Driven Design, write to us at hello@thenativeweb.io. These are the conversations we like having most.