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.
Focusing on CRUD API (Create, Read Update, Delete) and Entities force your end-users to perform the business logic and workflow themselves. Meaning they must know the logic since it’s in their head and not in your system. While CRUD sounds simple, I’ll illustrate how adding business logic to a CRUD-driven system can lead to a lot of complexity and how focusing on tasks instead can lead to a more explicit design that captures intent.
Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.
The predominant way that most people learn any type of web framework or how to write a basic web application is to pass around entities or data objects that represent a record in a database.
The Client makes a request to your application or service, and then it in turns queries your database to get data out of it. This could be using an ORM with a relational database or perhaps a document database.
The app service at this point may transform the data, depending on its shape, to an entity or some other type of data model. But usually, it represents a 1-to-1 mapping of an “entity” or database record. This is what will be returned to the client.
The client code will generally take this entity and provide the user with a UI form to update any relevant properties of the entity.
Once the user submits the form, that entity will be sent back to the Application/Service where it will update the record in the database.
This is pretty much the basics of any type of CRUD API. It focuses on “entities” or data models and Create, Read, Update, Delete operations around them.
For the most part, the application/service becomes a proxy to your database. Yes, it may have some superficial validation on the data or be handling your authentication, but really it’s just transforming data out and back into the database.
CRUD Gone Crazy
Below is a screenshot of a post on Reddit a few years ago that was asking if they should create a microservice per table.
In case you were wondering the answer to the question, my answer is no, absolutely not.
This does illustrate however the general focus on Entities and a CRUD API, and how so many blog posts, tutorials, videos are giving examples of this exact basic pattern around CRUD and Entities.
CRUD isn’t bad. It has its place. If you’re developing a small simple app, sure, by all means, use CRUD. However, if you’re developing a larger system with many different boundaries, you’re likely to have complexity in your domain that can’t simply be managed by typical CRUD apps.
Here’s an example of a product in a warehouse that is based on CRUD API. In this example, I’m specifically illustrating reading and updating a Product via an HTML form.
There is an ASP.NET Core Controller that handles returning the HTML View and then a route for handling the form POST for updating the product. A very basic Service-side rendering type of form.
The above has no complexity. However, let’s say we have a new requirement that we can only mark an item for sale if we have any amount of quantity on hand.
Well, we can implement that by doing that check in our controller when we set the ForSale property. No problem.
Let’s add some more logic to that to also specify that the price must be greater than zero in order for the product to be for sale. Again, no problem.
Here are the changes to the UpdateProduct.
While this example is trivial, this starts turning into a slow death by 1000 papercuts. As any system evolves, more and more complexity is added slowly over time. Unmanaged you can end up with so much complexity it makes it very difficult to change anything even with good test coverage because the setup for these becomes so complex.
Another issue is that all this logic resides in a controller that isn’t easily invoked by another application code. You cannot bypass this code.
However, a not so commonly talked about issue with the above is that this logic does not inform the user about the rules we’ve defined. It’s good that we now have this logic in our application, however, we’re not providing the end-user with any information about why the ForSale property isn’t being set.
If the end-user in the UI set the For Sale toggle to ON, but the quantity was set to 0, once they clicked save and were redirected back to the form, the For Sale toggle would be set back to OFF. They would likely think something is broken since it didn’t actually save what they intended.
Now you can overcome this by providing the end-users with validation warnings or errors. You can take CRUD pretty far, however in my experience when you’re in a domain with complexity this becomes death by 1000 papercuts and becomes unmanageable using CRUD.
The alternative to CRUD is being Task Driven. Provide your end-users with specific Tasks that represent explicit actions they take. With CRUD, the end-user has a specific goal in mind when they were editing the form. CRUD is very implicit.
If they were changing the quantity on hand, there was a specific business case they were doing that for. It could be because they did a stock count and have to do an Inventory Adjustment. An inventory adjustment is a specific capability you provide. Providing the ability to do an Inventory Adjustment is much more explicit.
Some portions of the UI can simply still be CRUD-driven. For example, the name and description are separate form for updating them. There is no business logic around those. The Price has various business reasons why it would be changed. Is it a price increase or a price decrease?
This may sound trivial but again making this explicit means you have explicit code that handles each task. You may want to publish an event of ProductPriceDecreased when a price decreases to email existing customers about the price change.
In code, this is reflected by organizing code by actual task.
Files are defined by Commands (State Change) or Queries (Return State). The AvailableForSale command looks like this:
You can also see that we’re not doing the state changes or logic to the product within the controller, rather doing it in the Product Entity itself.
CRUD API isn’t bad but it is a nightmare when you actually need to handle complexity. If you want to develop a system that moves logic out of end-users heads and lets them perform their job with the business capabilities they expect, then be task-driven and focus on business capabilities.
Developer-level members of my YouTube channel or Patreon get access to the full source for any working demo application that I post on my blog or YouTube. Check out the YouTube Membership or Patreon for more info.