Separation of Concerns is one of the key principles of software architecture that makes it easier to design, build and maintain scalable applications. In practice, it means that each piece of software has a single, clearly defined role that is distinct from another piece of the system. If that role cannot be defined in one or two words, then it is time to refactor.
By applying this principle at every level of the system we can turn old monoliths into a collection of services that are much easier to understand and maintain and therefore reduce the total cost of ownership over their lifetime. Couple this with a faster turnaround time for feature requests and the application becomes much more responsive to customer needs. In this blog I’m going to outline how to apply the Separation of Concerns to a monolithic application.
From Monolith to Microservice
With the rise of microservices we have gotten to see the best and the worst of the practice as people have launched into Trend Driven Development with varying amounts of thought and planning. This often leads to spaghetti code being spread out across multiple systems. These perform poorly due to the high network load each service creates by synchronously calling on many other services in order to fulfil a single request.
Breaking up a large application needs to be approached in a systematic fashion to avoid carrying over the mistakes of the past.
A Practical Approach to Breaking Up “The Monolith”
The easiest and safest way to tackle the monolith is to start from the outside and work your way in. Target the parts that already involve network communication first, as the relevant concern is easily identified and the performance impact is generally limited.
Web applications are already divided between frontend and backend by the network, so they are an obvious first candidate for separation. By moving away from server generated web pages to HTML5 style applications we can ensure the frontend only communicates with the backend via a clearly defined set of APIs. The largest benefit of this is that we can now support many clients without worrying about the implementation details of each client. Android, iPhone or browser, it makes no difference. The concern of display and human interaction is now isolated in the frontend while the maintenance of the state model is clearly a backend concern.
Backend Service Separation
Now we can look in the other direction at the external services our application consumes. The first targets for separation here are going to be third party services and datastores. Both of these aspects of an application already involve remote call across the network to obtain data. This means the performance impact of any extra calls is going to be much smaller when compared to replacing internal method calls with remote calls.
Third Party Facades
External services are inherently unreliable as they are outside of our control. By building a microservice facade we can present a consistent and reliable service to our application. When the external service changes or breaks, we can quickly adapt our facade without impacting or needing to retest our application.
Datastores are often misused as a back channel for sharing data within an application and between applications. The first target should always be data that is shared between applications. Authentication is a classic example of this, with SSO being one of the first microservices to gain widespread popularity even before people knew what a microservice was.
Once the application owns all the remaining data there are two choices for the data access concern. The first is to move the data access classes into their own module or library allowing access via an interface or service. Each interface should only contain methods that are logically grouped and described by the name. For example, a Client Interface would be expected to be able to perform operations with client data as that is the area of its concern. It would not be expected to return a list of flights from the local airport even if a client can travel on them. While not all choices are as clear cut as this it is also important to question each method to see if it belongs.
The second path to take is to split the persistence into its own service. This allows changes to the datastore without changing or retesting the main application as the persistence is hidden behind a constant API. For example, you can try using a NoSQL or GraphDB instead of classic SQL without changing the main application code at all.
Now to clean up the remaining parts of the application. It is often not worth splitting these up into separate applications, instead it is better to take the approach of internal services within the application. If in future there is new requirements for the application, it is then a simple task to split off the relevant concern into its own service.
You should first identify each concern within the application as this will form the base package layer. For example, controllers should be in their own package as they are solely responsible for responding to web requests using services exposed from other packages.
Microservices are a great way to scale applications efficiently and it can be a good opportunity to pay down some technical debt, by improving test coverage or documenting business requirements that may have accumulated over long lived projects. Using separation of concerns is a powerful way to guide the way we build microservices, however it’s not always easy to reconcile competing concerns the first time, so don’t be afraid to refactor.