Software Development Best Practice — #2 Loose Coupling

Milan Gatyás
Life at Apollo Division
7 min readOct 9, 2020
Photo by Markus Spiske on Unsplash

In the previous article “Software Development Best Practice — #1 Do Not Repeat Yourself”, I wrote about the benefits and reasons to follow the “Do Not Repeat Yourself” development best practice. In this article, let us talk about the second practice that I learned to be extremely valuable: Loose Coupling.

What Is Tight Coupling?

If you ever happen to see an angry Developer, with eyes wide open, swearing at everything around, there is a good chance he is trying to update a “spaghetti, tightly coupled code.” There are only a few things in the software development world that can get you more upset than when you need to rewrite half of the solution to squeeze in “that simple data source change.”

In terms of this blog article, what exactly does the “spaghetti, tightly coupled code” mean? It is the code where components, for example, business logic & code mix with the infrastructure specific code & models, often across the logical layer boundaries. Such structured code is very hard to maintain and the change requests are a nightmare to implement. Loose coupling is a principle which avoids writing a tightly coupled spaghetti code, helping to separate the business logic from the infrastructure logic, and encourages writing the code in isolated components with a clearly described interface. I will share a few stories from my development experience where tight coupling caused (or could cause) me severe problems, and also solutions to prevent this in your development practice. The examples are going to be in the .NET C# language; however, the principles demonstrated apply to the majority of available programming languages.

Reasons To Loose Couple

Let us start with an example from one of the first projects I was helping with. It had many tight coupling flaws; however, I will focus on one in particular. It was a legacy, thick client application, written in WinForms. The business logic code was present in the event handlers of the WinForms events, such as form load event, or button click event. This is an extremely bad practice, as the business logic was closely coupled with the WinForms nature of the application. The long-term plan, or let’s rather say a wish, was to migrate the application to the WPF framework. You can imagine that, due to the tight coupling issue, such a migration can be an extremely hard task to do. We had two major options to go with. Either have a fresh start to the project and reuse fragments of the legacy code or refactor the legacy application first, followed by migration to WPF framework. Neither of these scenarios happened in the timeline I was present on the project; the migration effort was just too great. The short-term benefit of being able to rapidly develop new features in tight coupled way paralyzed the solution and chained it to the WinForms environment.

This type of danger also applies to the backend service development world. In .NET environment, we developed the services as SOAP web services, which were replaced by WCF services, after which WebApi services came. Nowadays, the trend is to write containerized or serverless services in the cloud, and it is inevitable that this trend will change, too. Thus, it is important to not let infrastructure specifics leak into your business logic. Let us take a short example from AWS Lambda environment. Your .NET handler looks something like

You should make sure that OutputModel, InputModel, nor ILambdaContext makes their way into your business code. On the handler level, you should map the AWS environment-specific models into the business, infrastructure agnostic models, and pass these down into your application code. Similarly, business output models should be mapped to the AWS specific output models and returned from lambda.

When written in this way, the service component is working only with business models, which does not rely on any execution environment or specific infrastructure. You can take the same component out of AWS Lambda and put it into Azure Function. The only additional work required is the mapping between Azure models and business models.

How To Loose Couple?

So, what does a tightly coupled code look like?

MyService.ProcessData method is coupled with the SQSEvent model. This is not ideal, as the same data might be processed in the future from other data sources such as Kinesis Stream. Additionally, from the code design perspective, the MyHandler class is tightly coupled with the MyService class. This is also not a good idea as you might want to replace the implementation of the processing data method in the future. Also, it makes unit testing more complicated for the handler as you cannot abstract the service implementation away. What can be done instead is to apply inversion of control principle, where decision what service to use inside of the handler will be made outside of the handler. One of the most used techniques of inversion of control is the dependency injection pattern.

The code above is flexible enough that you can easily reuse the IService to process the data from other data sources without changing the service source code. It is also easy to replace or decorate the implementation of IService inside the handler without changing the source code of the handler itself.

Another type of tight coupling I want to describe is a very subtle, dangerous one. I will introduce it with a story that happened when I worked on a project in the past. An application used a CRM system to manage its users. Once a user was created in the CRM, the identifier was stored in the database together with the user record. The user was signing in by validating his credentials against CRM datastore. Other data tables existed too, carrying data that were queried based on the CRM identifier of the user. The project was growing for months and CRM identifier became one of the most central pieces of information in the system, having tons of dependencies on other types of data. However, as the userbase grew, we started to notice a performance downgrade of the connection to the CRM data store. We could no longer afford to communicate with the CRM data store in a synchronous manner. Changing the communication pattern to asynchronous meant we would not have the CRM identifier after the user creation process. Many data accessing patterns we already programmed would have been invalidated in that case. We needed to refactor the whole solution to decouple our application from dependency on the CRM identifier and CRM data store synchronous access. It took us over one year to do so and cost us an extreme amount of troubles and frustration.

The trap we fell into was that we coupled our data and access patterns to the external system which was not under our direct control. The application was totally dependent on the presence of a given CRM system and availability of synchronous communication. What we ended up with after the refactoring was that we instead relied on our application-specific identifiers. Every access pattern needed to be migrated to depend on the identifier which we generated and was fully under our control.

Loose Coupling Granularity

Lastly, I would like to mention the loose coupling granularity problem. Certain infrastructure specific libraries might be complex enough that abstracting away the usage into the infrastructure agnostic business models is too great an effort compared to the profit gained from it. One example I faced in my experience was the usage of the NEST library as a .NET client to the Elasticsearch engine. The classes and models to manage data indexes and data query language are very complex and trying to abstract it behind infrastructure agnostic models would basically mean writing dozens of classes and mapping logic to and from NEST infrastructure. Another example is the usage of Zuora subscription management platform in one application I worked on. The amount of platform-specific actions and models was extremely high and trying to abstract into deeper levels of platform specifics would bring danger that other platforms will have significantly different structures and abstractions would not fit the new platform structure. How to handle these scenarios? What proved to be the best approach for me is to find the tightest possible encapsulation level around the infrastructure specific code. For example, let us say we have a method for subscribing the user to the application. The method could look like

There is nothing subscription platform-specific in the ISubscriptionService contract, yet still we can unit test whether the subscription is made against the correct user, with the correct billing period, and so on. Service might internally need to do a lot of actions such as the creation of the rate plan, payment method, and, possibly, subscription account. However, these are platform-specific actions which might be different on different subscription management platforms.

I would like to wrap up this article with the final call to generally prefer loose coupling over tight coupling. Usually, short-term vision decisions of being able to deliver features quicker in a tightly coupled way hit you back months, or years later, but with multiplied force. In edge cases, it can bring the project to its knees, or spread despair in your team, causing it to disintegrate and vanish into the thin air. Think critically over your code design and when in doubt, sacrifice a bit of additional time of development and invest in loose coupling. I hope you got some valuable information from this article and see you in the next one.

Other from the series:

Software Development Best Practice — #1 Do Not Repeat Yourself

We are ACTUM Digital and this piece was written by Milan Gatyas, .NET Tech Lead of Apollo Division. Feel free to get in touch.

--

--