SOLID? Nope, just Coupling and Cohesion

How do we avoid writing spaghetti code so our systems don’t turn into a hot mess? For me Coupling and Cohesion. Some people focus on things like SOLID principles and Clean Architecture. While I don’t necessarily have a problem with that if you’re pragmatic, I don’t ever really think about either of those explicitly.

YouTube

Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything in this post.

Coupling and Cohesion

With over 20 years of professional software development experience, I’m mostly thinking about coupling and cohesion as a guide to software design. I’m not explicitly thinking about SOLID principles or clean architecture.

yin yang

Coupling and cohesion are like the yin-yang of software design. They push and pull against each other. You’re striving for high cohesion and low coupling, however, you’re trying to find a balance. We’re always fighting against coupling.

There are many different forms of coupling but to narrow it down for simplicity’s sake:

“degree of interdependence between software modules”

ISO/IEC/IEEE 24765:2010 Systems and software engineering — Vocabulary

At a micro level, this could be the interdependence between classes or functions, and at the macro level, this could be the interdependence between services.

When you’re thinking about spaghetti code you’re likely actually referring to a system that has a high degree of coupling at the macro and micro levels. This makes changes difficult to make or they break other parts of the system because there’s a high degree of coupling. Check out my post on Write Stable Code using Coupling Metrics for more on coupling.

Cohesion refers to:

“degree to which the elements inside a module belong together”

Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design

You can look at cohesion both from a micro and macro level. From a micro level, this can be viewed as how do all the methods relate in a class? Or how do all the functions of a module relate? From a macro level, how do all the features relate to a service? Check out my post on Highly COHESIVE Software Design to tame Complexity for more on cohesion.

But what does “belong together” mean? For me, this is about functional cohesion. Grouping-related options of a task. Not grouping based on data (which is informational cohesion). Yes, data is important, however, data is required by the functionality that is exposed.

Business Capabilities

Speaking of functionality, let’s jump to the Single Responsibility Principle for a second as this might clarify why functional cohesion is important.

When you write a software module, you want to make sure that when changes are requested, those changes can only originate from a single person, or rather, a single tightly coupled group of people representing a single narrowly defined business function. You want to isolate your modules from the complexities of the organization as a whole, and design your systems such that each module is responsible (responds to) the needs of just that one business function.

https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html

This might seem a bit more clear how the single responsibility principle at its root addresses coupling and cohesion.

If we focus on business capabilities and group them together, we’ll end up with a service. That’s why I always define a service as:

A Service is the authority of a set of business capabilities.

At a macro level, we’re trying to have a high degree of cohesion by grouping by business capabilities. Behind those capabilities is data ownership.

Unfortunately, many are still focused purely on data and what I call “entity services”. This was a screenshot from a post on Reddit. The question was if this data model diagram should be implemented as a service per “entity”.

entity services

This type of thinking is based on informational cohesion, not functional cohesion. The focus solely on data and not thinking at all about functionality and behavior leads to design generally around CRUD.

As an example, you’ll often have “Manager”, and “Repository” classes that look similar to this.

IProductRepository

The Product “entity” is really just a data model with a bunch of properties. There is no behavior exposed, they are just data buckets.

So where is the actual functionality of a product? In a large system, for example in a distribution domain, a product isn’t just a product.

A product means different things to different people within that domain.

Service Boundaries by Functional Cohesion

Sales are thinking about the Sale Price of a product and are customer-centric. Purchasing/Procurement is thinking about the cost and is vendor centeric. The warehouse is concerned about shipping and receiving. They all have different business capabilities, and because those capabilities are different, they care about different data behind those capabilities.

A product isn’t just a product that has to live within a single boundary. It’s a concept that can live within multiple different service boundaries.

Organizing related business capacities (features) into services allows us to decide how we want to handle technical concerns within each service or to a specific feature. Dependencies shared concerns such as data access, validation, etc. All of these decisions can be localized and made per service.

Services with features with high functional cohesion

Loose Coupling

So you’ve defined boundaries based on functional cohesion, but they must have some interdependence between each other?

Tight Coupling between Services

Having a free for all where any service can be coupled to any other service is still a hot mess of spaghetti. In other words, tight coupling. The system will still be hard to change and fragile to change.

You want to remove the tight coupling by having service boundaries be independent and not directly coupled to other services. One way of achieving this is through loose coupling provided by messaging.

Loose Coupling between Services

Because you’re grouping by business capabilities, and the data behind those capabilities, each service should have all the data it needs. This means that you aren’t coupling services because you need to fetch data from them to perform an action.

Services together need to create workflow and exchange information. As an example from the diagram above, if the Warehouse has a “Quantity on Hand”, you might think that Sales would need that so it knows if it can sell a given product. However, Sales actually has its own concept called ATP (Available to Promise) which is a business function that is the projected amount of inventory it can sell. This consists of what’s in stock in the Warehouse, not allocated existing orders (Invoicing), as well as purchase orders and expected receipts (Purchasing).

Sales can maintain their ATP by consuming events from other services. It does not need to make directly coupled to an API to make calls at runtime to calculate ATP for a given product. It maintains and owns the ATP for all products based on events it’s consuming that are published from other services. When Invoicing publishes an OrderInvoiced event, it can subtract an amount from ATP. If the warehouse does a stock count and publishes an InventoryAdjusted event, Sales will update the ATP accordingly.

Responsibility

From a lower-level code perspective, the coupling can be challenging when we have to deal with many different technical concerns. Web, Authorization, Validation, Data Access, and Business Logic. But as mentioned earlier, each feature/capability can define how each of these concerns is handled.

While you can share between features, such as a domain model, this naturally starts limiting the coupling between a set of features.

One approach to handle the common concerns is by using the pipes & filters pattern. Specifically, the Russian doll model which the request passes through a filter that can call (or not) the next filter. This allows you to separate various concerns and create a pipeline for a request.

Pipes & Filters

For more check out my post on Separating Concerns with Pipes & Filters

Coupling and Cohesion

Building a large system without any boundaries (low cohesion) and a high degree of coupling is ultimately building a big turd pile of a system.

Big Turd Pile

It will be hard to add new functionality, and hard to change existing functionality without breaking and causing regressions.

Focusing on having highly cohesive services that provide specific business capabilities and loosely coupling between those services to provide workflow and business processes will allow you to build a system that is more resilient to change.

Smaller Turd Piles

Decomposing a large system into smaller independent logical service boundaries allows you to make different decisions that are localized to an individual service. No system will be perfect, especially over time and it will need to evolve. Focus on coupling and cohesion as a guide.

Join!

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

Follow @CodeOpinion on Twitter

Software Architecture & Design

Get all my latest YouTube Vidoes and Blog Posts on Software Architecture & Design

Is an ANEMIC Domain Model really that BAD?

Is an anemic domain model a bad thing? Most would probably call it an anti-pattern, and it should be avoided. But is that really true? Well, it depends on what your intent is. Are you trying to create a domain model? Or are you really just trying to create a data model?

YouTube

Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything in this post.

Example

In this post, I’m going to be using the example of a food delivery service. You order food from the restaurant and it’s delivered to you.

There is the concept of a shipment which is the delivery of your food order. Every shipment has two stops. The first stop is the Pickup which is the restaurant, and the second stop is the Delivery which is your house/apartment/work or wherever you’re getting the food delivered to.

Conceptual Model

Each stop (Pickup and Delivery) goes through a progression from being In-Transit, Arrived, and Departed.

Stop Progression

The first Stop must go through this full progression before the second Stop can start. This makes sense because the food delivery driver must first Arrive at the Restaurant (the first stop). Once they pick up the food, they depart. Once they have departed the First Stop, then they can Arrive at your house/apartment/work for the delivery. Once they deliver the food, the second stop is now Departed.

Anemic Domain Model

An anemic domain model is an object model that contains a data structure but no behavior. In the case of my shipment example, this means it has an object for the Shipment and the Stops, but no behavior within the objects to make the stop progressions.

While there are a few methods for setting data, this isn’t behavior. We need logic to control the Status on a stop. For example, we can’t change the Stop Status on the Delivery Stop to Arrived if we haven’t Departed the Pickup Stop.

There is no behavior, my object model of Shipment and Stops are just data buckets.

So where does the logic live? In an anemic domain model, generally, you’ll find these in a “Service” or “Manager”. So in this example, you’d likely have a ShipmentService or ShipmentManager that contains the logic.

In the ShipmentService above, we do have logic in various methods for validating that the current state is valid before we perform a state change. Also, note that most of these methods are taking the shipment as a parameter however sometimes I see data access done directly within these methods.

Regardless of where the data access is, the point is that the logic resides separately from the data. The object method of Shipment and Stops are just data buckets and contain no actual logic. All of the logic is separated and contained within inside the ShipmentService which then mutates the Shipment and Stops.

So what’s the issue with this?

It depends on what your intent is. If you’re intent is to make an object-oriented domain model, then this isn’t it. You’ve made an object method that represents the data structure. You’ve made a data model. You think you have a domain model, but you really just have a data model.

Now I suspect most people aren’t trying to create a domain model, what they’re really doing is just trying to create some separation or layering (more on this later in this post). What you’re really closer to are Transaction Scripts.

Entity Services depend on an ORM for data access. The ORM provides returns Entities that are really a one-to-one mapping of a table structure if you’re using a relational database.

Anemic Domain Model

Spectrum

Now I actually view most anemic domain models closer to Transaction Scripts than I do a Domain Model. Anemic domain models are kind of this in-between phase of going a bit further than a Transaction Script but not all the way to a Domain Model.

Transaction Script to Anemic Domain Model, to Domain Model

Transaction scripts are much more procedural and often times has many more mixed concerns, such as data access, validation, etc.

The intent of a transaction script is to handle a single request. It contains everything. It would contain the data access to get out the Entities/Domain Objects. It would contain all the validation logic and also state changes.

Transaction Script

Because you have many mixed concerns, people want to separate these concerns and land with Anemic Domain Models.

Where this starts falling apart is when you need to duplicate logic or state changes that exist in multiple different transaction scripts. When you start seeing this duplication, you then want to start having transaction scripts call other transaction scripts so you can reuse them and stop duplicating code.

Transaction Script calling another Transaction Script

This becomes difficult because if they both contain their own data access logic, then there’s often no simple way of sharing the same underlying transaction.

Once people hit this point, this is where I believe they start going down the Anemic Domain Model path. Instead of duplicating code in different transaction scripts, that’s shared within a single class that turns into the Service (eg, ShipmentService).

Domain Model

The alternative is to have a domain model that exposes behaviors and encapsulates data. Don’t separate the behaviors from the data like you are with an anemic domain model. Keep both behaviors and data within the same object model.

Domain Model

Similar to a transaction script, it can be the one delegating what to do. It can provide the data access to retrieve and/or build our domain model (often an Aggregate Root). It can then call the relevant public methods on the domain model to perform the requested command/action.

The constructor takes a list of Stops. The only way a Stop can be mutated is by calling methods on the ShipmentAggregateRoot. We’re hiding (encapsulating) the data and only exposing behaviors.

To invoke our domain model, we have command handlers that get our Aggregate Root via a Repository, call the appropriate method, then save the aggregate.

Similar to a transaction script, it’s handling a single request, but now instead of having mixed concerns within it, it’s simply delegating to data access and our aggregate root.

Anemic Domain Model

Is an anemic domain model a terrible thing and an anti-pattern that you should avoid? Well, it depends on what your intent is! If you’re trying to create a domain model because you have a lot of complexity, then yes it’s an anti-pattern. However, if you don’t have a lot of complexity that warrants a domain model, a transaction script might be better served for simplicity.

If you’re using transaction scripts and things start getting more and more complex or you start duplicating logic across transaction scripts, then start thinking about building a domain model that hides (encapsulates) data and exposes behaviors.

Join!

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

Follow @CodeOpinion on Twitter

Software Architecture & Design

Get all my latest YouTube Vidoes and Blog Posts on Software Architecture & Design

CRUD API + Complexity = Death by a 1000 Papercuts

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.

YouTube

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.

CRUD API

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.

CRUD API + Complexity = Death by a 1000 Papercuts

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.

CRUD API + Complexity = Death by a 1000 Papercuts

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.

Complexity

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.

Task Driven

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.

CRUD API + Complexity = Death by a 1000 Papercuts

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.

Complexity

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.

Source Code

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.

Follow @CodeOpinion on Twitter

Software Architecture & Design

Get all my latest YouTube Vidoes and Blog Posts on Software Architecture & Design