Skip to content

CRUD vs Task Driven: Are You Losing User Intent?

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.


What happens when everything in your system just becomes an update?

You need to cancel an order: change the status. You need to approve something: change the status. Some workflow completes: just change the status.

Everything becomes an update.

And it can feel great because it is simple, but you have lost one really important thing: intent.

You know what changed, but you do not really know why it changed. You can try to infer it, but you do not really understand what the user was trying to do and why they were trying to do it.

That is where things start to fall short.

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.

Are You Updating Data or Performing an Action?

Imagine a shipment with a pretty simple edit form. We can change the status, pieces, weight, dates, and so on.

But then the customer calls in and says they want to cancel the shipment.

What exactly are we doing here?

Maybe there is a “Delete Shipment” button. Is that a hard delete? If so, we are losing data. Maybe it is actually a soft delete. But if we are using a soft delete to cancel the shipment, the UI is really lying to us because we are not actually deleting anything.

Maybe canceling is just changing the shipment status to “Canceled.”

But is that really what we are doing?

When we cancel a shipment, maybe we need to capture a bunch of information that is relevant to the cancellation. Who canceled it? What was the reason? Who requested it? Are there additional notes?

Are we deleting the shipment? Are we changing a status? Or are we actually capturing the information and intent of canceling the shipment?

Those are very different things.

CRUD Is Not the Problem

The obvious reaction to this is, “Well, CRUD is bad and everything should be commands like CQRS.”

No. That is not the case.

CRUD is incredibly helpful and useful when you are actually just managing data.

The problem arises when you try to force CRUD where you should not, and you really have business capabilities. The opposite is also true. You can force everything into commands when what you really have is simple CRUD.

The question is not, “Should it be CRUD or task driven?”

The real question is:

Do you just have data that you need to manage, or do you have business capabilities where you need to capture intent?

Most systems need both.

Why CRUD Is So Appealing

Building CRUD is much simpler because there is so much tooling, frameworks, and libraries built around it.

You have a client calling an HTTP API. REST, although a lot of the time it is not really REST. It is basically JSON over HTTP.

The API hits a database, gets some type of record, and returns that record to the client. The client changes some fields and sends essentially that same data model back, just mutated.

Maybe that happens through PUT. Maybe partially through PATCH, because this is a REST API apparently.

Then we update the data.

It is simple.

A lot of the tooling is effectively trying to map HTTP methods to CRUD operations in SQL. GET becomes SELECT. POST becomes INSERT. PUT and PATCH become UPDATE. DELETE becomes DELETE, whether that is a hard delete or a soft delete.

In our shipment example, we could have a set of URIs and HTTP methods mapped to operations that modify shipment data.

There is nothing inherently wrong with that.

Objects as Resources

I like a term Alex Moore uses for this: objects as resources.

Again, there is nothing inherently wrong with this.

It makes it simple to build a UI on top of your database, and that can be really productive because of all the tooling available.

If all you are really trying to do is manage data, this works extremely well.

Sometimes CRUD Is Exactly What You Want

A good example from a logistics application is a customer.

You can edit a customer. It is CRUD.

You can change their email address, primary contact, phone number, address, or other information.

We do not necessarily need a “Change Email” command or a “Change Primary Contact” command.

Why?

Because if we do not have any meaningful behavior around those changes, we do not need to go through the extra ceremony of creating commands for them.

It is CRUD. It is simple.

When CRUD Starts Falling Apart

The trouble starts when your data model and your resource model are the exact same thing.

You have your data model, return it to the client, let the client mutate it, and send it back.

That works fine until there is actual behavior behind those changes.

The problem is that the behavior often exists only in the end user’s head. It is not actually represented in the system.

As far as the software is concerned, the user is just changing properties.

That is where things start falling apart when you actually need to understand what the user is doing and why they are doing it.

A Shipment Is More Than a Status

Consider part of a shipment workflow.

The first thing that happens is an order is dispatched. We have a date and time related to that, a vehicle or carrier, and other information about when it was dispatched.

Next, the vehicle arrives at the shipper. Again, we capture the date and time. This represents the vehicle physically arriving at the location where it needs to pick up the shipment.

Then the freight is loaded. We capture pieces, weight, bill of lading information, and other relevant information. This represents the freight actually being put onto the vehicle.

Finally, the vehicle departs the shipper and is now on route for delivery.

What do we have here?

We have outcomes.

All of these different events represent explicit outcomes as part of our workflow.

Why?

Because we had explicit tasks.

We dispatched an order. We arrived at the shipper. We loaded the freight. We departed the shipper.

It is a combination of tasks guiding the user through the workflow.

Because those tasks are explicit, the outcomes are also explicit.

The UI can reflect that. Each individual task can visually guide the user through the workflow and ask for the information required for that specific action.

When performing “Freight Loaded,” for example, the user enters the loaded weight, number of pieces, bill of lading number, and whatever other information is required.

That is very explicit.

Now we can capture that action and produce an event representing its outcome that downstream services can react to.

“Status = Loaded” Is Not the Same Thing

Compare that with simple CRUD where somebody edits the shipment and changes the status to “Loaded.”

Do we know what the bill of lading information was?

Can we require it?

Can we validate the loaded pieces and weight?

Can we derive a meaningful event from this change?

There is a big difference between editing a shipment and changing its status, and explicitly recording that the freight was loaded.

That difference is intent.

Design Around Business Capabilities

The important part about designing around tasks is thinking about business capabilities.

What does the system actually do?

What are users actually trying to accomplish?

Dispatching an order, arriving at the shipper, loading freight, departing, delivering, and canceling an order are explicit actions users are trying to complete as part of a workflow.

Often there is real business logic and validation behind these actions.

Maybe one action can happen only after another action. Maybe there are different validation rules. Maybe we need to check the variance between the ordered number of pieces and the number of pieces actually loaded.

There is legitimate behavior involved.

If you actually have behaviors like this, that is where you want to capture them explicitly as part of the workflow.

Do Not Turn Everything Into a Command Either

You can take this way too far.

“Change Customer Name.”

“Update Customer Phone Number.”

“Modify Customer Address Line 2.”

What meaningful behavior do we have around any of this?

Maybe none.

You do not get bonus points for creating 25 commands for one screen that really should just be simple CRUD.

This is why treating CRUD versus task driven design as some type of architectural ideology misses the point.

You should use the approach that actually represents what the software is doing.

Most Systems Need Both

Most systems need both CRUD and task driven workflows because not all boundaries are created equal.

In a logistics application, compliance might mostly involve maintaining data, so CRUD works perfectly well.

Dispatch and ordering might involve explicit workflows with meaningful business behaviors, so they are much more task driven.

A CRM or finance integration might mostly involve maintaining mappings because the external systems contain the actual behaviors.

Even within something like ordering, there will probably be portions of the system that are simply CRUD setup data.

There is no reason all of these things need to use the same interaction model.

What Is the User Actually Trying to Do?

The real question is not:

Should this be CRUD or task driven?

The real question is:

What is the user trying to do?

If they are simply maintaining data, use CRUD.

If they are performing business capabilities as part of a workflow, and those actions have behavior and intent you want to capture in your business logic, model those actions explicitly with tasks and commands.

Most systems need both.

The mistake is assuming everything is just an update.

Because once everything becomes an update, it gets very easy to know what changed while completely losing why it changed.

And that “why” is often where your actual business domain lives.

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.