Must we convert monoliths to microservices?

David Garcia
21 min readFeb 5, 2021

--

Source: “Must we convert monoliths to microservices?” @ linkedin.com

This article was written originally by Graham Berrisford at LinkedIn, who has given written consent to re-post it here. All credits belong to Graham. Please review the Resources section at the bottom of the article for further links.

An architect recently complained his CIO uses the term “microservices” as meaninglessly as “SOA” has long been used. On this topic (like so much in IT) some nonsense is talked. And the use of a design pattern in marketing by technology vendors may be read with scepticism.

What’s new? The trend is towards distribution and flexible deployment of code. Application layer components can be deployed in “containers”, which are shiftable between servers and managed separately. Here’s a design pattern.

Source: “Must we convert monoliths to microservices?” @ linkedin.com

OK, but is it always advisable to divide a monolithic application into microservices? Some do it because they think it will optimize their use of cloud resources and allow for infinite scalability, where one of more of the following applies:

  • it is not necessary — their application is not remotely like Amazon
  • it consolidates a sub-optimal or inefficient modular design
  • it needlessly burdens messaging and network technology
  • it complicates the code, and deployment of the code
  • it complicates management of the run-time.

And in worst case scenarios, it leads to the 4Ds (see below) and complicates business operations, requiring “compensating transactions” to undo uncompletable transactions.

Five motivations to be discussed

  1. To maintain data stores already distributed and separately managed
  2. To separate what must be coded using different technologies
  3. To enable scalability or very high throughput
  4. To migrate a large legacy application for operation in “the cloud”
  5. To facilitate agile development

Four trade offs to be discussed

  1. Agile development trade offs
  2. Sociological trade offs
  3. Modularity trade offs
  4. Data management trade offs (the 4Ds)

A few generalizations

Most enterprise applications are client-server in nature. The work they do is transactional data processing. Many transactions create and use data contained one or more data stores

Large applications have always been modularized. Modules are assigned to different teams. And teams work out ways to share what they can. Generally speaking:

  • Design patterns that dismember a cohesive data structure should be resisted. Dividing a data structure, and storing it in two or more discrete data stores, adds complexity.
  • Design patterns that dismember a business transaction should be resisted. Committing part of a transaction, without checking all necessary state data, adds complexity.
  • If you must dismember data stores or transactions — to scale out app/data servers to handle very high transaction throughput rates — then the optimal design is more procedural (one module per transaction) than more object-oriented (one module per persistent data structure).

So, generally speaking:

  • Where the contents of different data stores (e.g. A, B, C above) are readily combined in one coherent data structure, then do it — for integrity, simplicity and cross-data store analysis.
  • Where transaction throughout is not predicted to be extraordinarily high, do not let design for scalability drive the software design.
  • Where transaction throughout will be extraordinarily high, and it is necessary to minimize resource use, than encapsulate each transaction in its own component/module/service, and scale each to the demand for that transaction.

How small is micro?

How big or small is a microservice? It does not have to be small; it only has to be smaller than the alternative monolithic design. If there is no conceivable monolithic alternative, then a microservice is nothing more or less than a service.

Smaller systems may be easier to build and maintain than large ones. But all is trade offs. Given one system to design:

  • reducing the size of its components
  • increases the frequency and complexity of messaging between components.

Given those small components must be joined up to make one application, you still have the same requirements to meet:

  • the same use cases/transactions to process
  • the same business rules to code, and
  • the same persistent data to maintain.

Two design contexts

In the first case below, a monolithic application is impractical. In the second case, dividing one application into several is a design option, not mandatory.

1) Is the macro requirement for use cases that access several distinct data structures, currently or naturally stored in different stores, perhaps using different technologies?

If yes, then the whole application is a mash up discrete applications (or application components) behind an API Gateway. Fine, but there is no plausible “monolithic” application in this case.

2) Is the macro requirement for use cases that access one coherent data structure, which can readily be stored in one data store?

If yes, then division of the application layer into components — coded separately — has been a common practice for decades. But dividing the data layer into discrete data stores can create difficulties (the 4 Ds below) you might rather do without. And it can lead to needlessly complex and frequent messaging between microservices.

Some definitions

Application: the whole set of software that supports a business role or process by offering a cohesive set of use cases (or user stories) to users — via what they perceive to be one cohesive user interface. Typically, it is tested as a whole before deployment.

Monolithic application: an application whose application layer code is deployed in one unit. It may be modularized in various ways. Whatever the programming language, you could modularize the application layer into one component for:

  • each procedure — each transient client-server transaction, and/or
  • each data object — each persistent data entity that is maintained.
  • a compromise in which coarse-grained aggregate data objects each handle a group of transactions.

In a monolith, all modules execute in one “process” and can communicate with each other using some kind of local method invocation or function call. So, a module cannot be deployed on its own, or invoked (without its sisters) from another application.

A microservices architecture is a kind of service-oriented architecture in which what could be coded and deployed in one “monolithic” application layer, is divided into several separately deployed components. The microservices architectural style was defined on Martin Fowler’s web site (2020) as “an approach to developing a single application as a suite of small services.

  • each running in its own “process”
  • communicating with lightweight mechanisms, often an HTTP resource API.
  • built around business capabilities [more accurately, business functions]
  • independently deployable by fully automated deployment machinery.
  • [with] a bare minimum of centralized management of these services,
  • [possibly implemented using] different languages and data storage technologies.”

(For the connection of microservices to business functions read this article on business activity modelling.)

Microservice: a component or module in the application layer of what could be a monolithic application — that is decoupled from sister microservices in at least the first and possibly all of three ways

  1. Decoupled by location: a microservice is deployable in/on its own container/server (so scalable on its own)
  2. Decoupled by time: microservices communicate over a network using an asynchronous protocol or messaging technology. (This 2nd condition doesn’t rule out that there is — logically — a synchronous dependency between microservices.)
  3. Decoupled by data encapsulation: a microservice accesses data entities maintained by sister microservices only via their APIs. (This 3rd condition is debatable, open to interpretation, and discussed in sections below.)

A simple monolithic application

Suppose our logistics application maintains a persistent data structure. There are 3 kernel entities: location, movement and resource. Below is a simplified version; imagine there are 33 entity types in total.

Source: “Must we convert monoliths to microservices?” @ linkedin.com

Modularizing the application layer

The application layer is divided into three sister modules, each named after the data it maintains. Each receives transactions that act on its own data. Each is further modularized internally, but those finer-grained modules (or objects) are not of interest here.

Source: “Must we convert monoliths to microservices?” @ linkedin.com

The facade directs client-to-server transactions to the appropriate module. Transactions centered on creating or accessing a location data entity are directed to the location module. Transactions centered on creating or accessing a movement data entity, are directed to the movement module. Transactions centered on creating or accessing a resource data entity are directed to the resource module.

All three modules access the same data store.

Cross-module transactions and inter-module dependencies

Some transactions can be processed entirely within one module; others require to access more than one kernel data entity. Inter-module dependencies may be bi-directional. Creation and deletion dependencies tend be in opposite directions.

  • Creation dependencies: typically, more volatile data depends on more stable data. The birth/creation of a transient entity often depends on existence or state of a more persistent entity. E.g. to create a movement scheduling transaction, the movement module must check the locations are open on the day of the movement, and check the resource is not already scheduled for movement in the same time period.
  • Deletion dependencies: typically, the death/deletion of a persistent entity depends on existence or state of more transient entities. E.g. a location cannot be removed or closed while there are movements scheduled to or from that location.

Three design options for cross-module transactions

Suppose a transaction directed to the movement module needs to access or even update data maintained by the location and resources modules. Design options include:

  1. More procedural: the movement module directly accesses location and resource data in the data layer (without referring to a sister module).
  2. More object-oriented: the movement module invokes its sister modules to access location and resource data.
  3. Data duplication: the movement module accesses its own “cache” of location and resource data.

A monolithic application designer might pick either option 1 or 2, using synchronous method/function calls between modules.

A microservices designer (as shown later) may divide the data store into three, then choose either option 2, using an asynchronous messaging mechanism between modules, or option 3, which implies the need for additional data synchronization processes.

Degrees of code isolation

For more a technology/tool-oriented discussion of modularization options, watch this video It neatly tabulates different degrees of “code isolation” as shown below (here, the term “module” is technology specific.) The recommendation is to start with the simplest option on the left, and move from left to right only when needed.

Source: “Must we convert monoliths to microservices?” @ linkedin.com

Remember three things:

  • as Craig Larman warned, decoupling today for a tomorrow that may never come is time not well spent,
  • decoupling physically is not decoupling logically,
  • inter-module dependencies may be bi-directional.

Suppose you separate the application layer modules as shown in the right-hand column of the table above, then you have turned your monolithic application into micro services. You may go further by “decentralizing data management” (Fowler), as shown below.

Converting the monolithic to microservices

The graphic below reshapes our monolithic application using a design pattern in the Microsoft Azure documentation. Notice that persistent data has been divided into three data stores. Each contains one kernel entity and 10 related entities.

Source: “Must we convert monoliths to microservices?” @ linkedin.com

This technology vendor diagram doesn’t show whether or not the three sister microservices are decoupled by time, location and data encapsulation — though you might guess those decouplings are implied.

Whether one microservice in the graphic can run in production on its own, or be invoked from a different application, depends how closely it depends on the sister microservices it is supposedly decoupled from, but is usually still related to in some way(s).

This article goes on to outline five motivations and four trade offs related to microservices.

Five motivations

What is the motivation? Why divide an application into microservices? Of five motivations that have been advanced, two are interesting, two are not, and one is questionable.

-1- To maintain data stores already distributed and separately managed

This is not interesting, because it describes the application portfolio most enterprises already have. And integrating discrete applications is a system integration task as it ever has been.

-2- To separate what must be coded using different technologies

This is not interesting, because such application components are naturally discrete and separately developed. And it is advisable to not to mix more technologies than you need.

-3- To enable scalability or very high throughput

Interesting. Is scaling really needed? Surely few applications need to scale beyond what a conventional database application can handle? Using solid state drives and in-memory data storage, an application might handle as many as 20,000 transactions per second.

Most businesses are unlike Google or Netflix. Surely few of their applications have a throughput high enough to require division to parallel microservices for that reason?

Supposing scalability really does matter, how to divide an application into modules that are scalable perfectly to match demand?

Surely the optimal design is to code each transaction as a procedure in its own microservice? (The transaction scripts might duplicate what could otherwise be factored out into common subroutines, since exceptional requirements require exceptional designs, for which a price must be paid). Then, each transaction module can be deployed and scaled up or down to match the demand for that particular transaction type.

Does that fit the microservices architectural style Fowler had in mind?

-4- To migrate a large legacy application for operation in “the cloud”

The much-touted idea is to deploy application components (in a legacy application’s application layer) in “containers” that can be shifted between servers, and managed separately. The question is — why? And will migrating applications to the cloud merely consolidate and inefficient software design patterns?

Suppose your legacy monolithic app is running fine, it is monitored in real time and secure. Do you need to migrate into the cloud? Suppose the answer is yes — you are convinced migrating the application into the cloud will be beneficial. On to the next question: must you divide the application layer micoservices? And if yes, should you simply deploy the modules as they are — or refactor them?

See this article on IBM’s work to automate application modularization. It does little to “refactor” the modular structure of a legacy application. It merely draws boundaries or “seams” between modules/objects in the legacy application structure — then allows designers to group them into larger modules — and to be separately deployed behind APIs.

The article does give some reasons for refactoring a legacy application into microservices, but while all seem rational, all may be questioned, and some feel like healing a self-inflicted wound.

  • “Lift and shift does not scale.” Is scaling really needed? (See motivation 3 above.) Again, to scale up to handle a high throughput transaction, surely the optimal design would be to code that transaction as procedure in its own microservice? And if your legacy application is already modularized in a different, more object-oriented, way, then IBM’s refactoring algorithm will consolidate that sub-optimal design?
  • “A large legacy application is very difficult to administer in the cloud.” Then why deploy it in the cloud? And why is administering scores of interconnected microservices simpler? If the legacy application didn’t need automated start up and load balancing before, why does it need it now? If that is a consequence of division into micoservices, then it sounds like healing a self-inflicted wound.
  • “Developers had to restructure millions of lines of code into smaller microservices through an expensive, time-consuming, largely manual and often error-prone process” Did migration to the cloud really require them to do that? Because cloud servers are so expensive, their use must be scaled down to the minimum? Or was restructuring a naive strategic mandate from a CTO, CIO or EA?
  • “The goal is to leverage the cloud’s inherent operational efficiency, economic, agility and security benefits.” Wrt efficiency and economy: Does the migration slow transaction processing by a) replacing local or synchronous calls by remote calls or asynchronous messaging? b) increasing network traffic and use of messaging technologies? Wrt agility: Does migration leave the underlying database untouched, so microservices are coupled to each other via that database? If two microservices handle two transactions that both update the same data entity, then they cannot be run properly without each other, and data management is not decentralized. Does that fit what Fowler calls the microservices architectural style?

-5- To facilitate agile development

This seems a common motivation — to divide work between smallish teams that can each develop and deploy a microservice quickly, then maintain and extend it with minimal disruption to what other teams are doing. Typically, the data store of a microservice can only be accessed via its API , but see the trade offs below, and remember the 4Ds.

Four trade offs

Microservices is not inherently bettter than other design patterns. The pattern can be used well, and can be used very badly. All is trade offs, and all the classic design trade offs apply. I don’t reject any design option, only set out trade offs to be considered.

-1- Agile development trade offs

It is important to recognize that agile development principles can be in conflict.

One classic trade off is between keeping it simple and decoupling components for separate development. Decoupling physically what is logically-coupled leads to the 4D complications discussed below.

Another classic trade off is between “you ain’t gonna need it” and “design for future flexibility”. Suppose you build version 1 of an application on top of the slimmest, simplest, possible database structure. The API of the application encapsulates that data structure. But naturally, changes to the data structure are likely to be reflected in changes to the API.

Predictably, it turns out that version 2 requires the database to be extended and restructured. For as long as the persistent data structure is growing and evolving, then (however you modularize the application) it may prove difficult to encapsulate application modules behind stable APIs, and isolate one module from changes to another.

Once the persistent data structure of an application is stable, agile software development is a natural way to proceed. Before then, refactoring costs can be considerable.

-2- Sociological trade offs

As Conway pointed out in 1968, development team structures tend to reflect software architecture structures, and vice-versa. Still, software sociology — assigning different application components to different teams — cannot remove dependencies between those components.

The sociological benefit to development team operations of decoupling microservices can (by allowing data disintegrity) prove costly to business operations. Business process issues arise when data maintained by one microservice is not up to date and consistent with data maintained by other microservices. Asynchronous integration and stale caches create the risks of:

  • scheduling to move a resource to a location — on a day that location is closed
  • booking a hotel room — for a day it has already been booked
  • failing to recognize a suspect has a criminal record.

Allowing such a mistake to happen leads to the complexity of having to design and implement “compensating transactions” to recover from the mistake. Sometimes recovery is impossible.

-3- Modularity trade offs

Simpler and smaller components means more frequent and complex inter-component messaging.

  • Larger modules: Fewer, longer, processes. Less messaging. Looser coupling.
  • Smaller modules. More, shorter, processes. More messaging. Tighter coupling.

In a microservices architecture, as the complexity of the architecture grows, it can become harder to see which module or component is having issues. Understanding what’s happening in the system can be difficult because any slow down or error in one component may affect other component(s).

Excessive modularization and decoupling leads to excessive messaging.

In the worst example reported to me, dividing one e-commerce application into 200 microservices meant that a single client-server transaction could require scores or hundreds of messages between microservices. Following advice to deploy those microservices on different servers, and connect them via a message broker, the complexity and performance overheads were staggering. They soon ran out of space to store the event log. One business event had become countless technical events of no practical interest or use to the business.

Four way to reduce the excessive messaging.

Undo some decoupling.

  • Co-locate closely-related components. Convert asynchronous calls to synchronous calls.

Group objects/modules into larger ones.

Group small messages into larger ones.

  • “The biggest issue in changing a monolith into microservices lies in changing the communication pattern. A naive conversion from in-memory method calls to RPC leads to chatty communications which don’t perform well. Instead you need to replace the fine-grained communication with a coarser -grained approach.” Martin Fowler

Refactor more object-oriented modules into more procedural transaction scripts.

-4- Data management trade offs (the 4Ds)

Decentralization of data management

Jeff Bezos is in favor of local agility and encapsulation of data behind APIs, and agin’ shared memory. His famous mandate speaks of barring any “dirty read”. I read this as meaning application development team A must not read data in a data store maintained by team B without going through the API published by B.

Martin Fowler is for local agility and decentralization of data management and agin’ enterprise architecture and anything enterprise-wide. His idea is to subdivide an enterprise’s persistent data into smallish data structures, and assign ownership of each structure to one microservice, whose API is the single source of truth for that data’s current state.

However, challenges arise wherever there are logical relationships between entities in different subsets of the overall data schema (or referential integrity rules between tables) and (for this or other reasons) data is or must duplicated in different data stores.

Among many design options are these three:

  1. More procedural: integrity rules are maintained in the data layer, likely by the DBMS. This means that, under the covers, that one microservice accesses data maintained by another (and may even update it). Also, since the several microservices are coupled to one data store, this centralization of data management challenges the vision of separate development, testing and deployment.
  2. More object-oriented: the macro data schema is divided into micro data schemas. And each microservice maintains its own data store. This doesn’t remove the need to maintain relationships between data in different data stores. It merely moves the relationship from the data layer into the application layer. The application components are coupled by application layer messaging.
  3. Data duplication: each microservice holds a “cache” of whatever data it needs that is maintained by the other microservices. This implies additional data synchronization processes to refresh that cached data, now and then.

The second and third options can lead to one or more of the 4Ds:

  1. Duplications: caching of data from one data store in another.
  2. Disintegrities: and the complexity of compensating transactions to restore integrity.
  3. Delays: processes are slowed by the need for inter-application messaging.
  4. Data analysis difficulties; where a report requires data from several data stores.

Reader comments and questions

A reader asks: How to scale an extremely object-oriented design to optimize the resources used by transactions? Every case is different. How about we ask:

  1. What percentage of application dev and maintenance cost is spent on server resources?
  2. How wide are the transaction throughput variations?
  3. How predictable are those throughput variations?
  4. How much cost will be saved by spinning up and tearing down containers?

Different answers could lead us to:

  • scale up/out to the maximum possible throughput
  • schedule scaling up/out for specific days/times
  • refactor the code to encapsulate the transactions rather than the objects
  • buy a mainframe?

A reader tells me: “a good data virtualization tool can take care of cross data store duplications, disintegrities and data analysis as if it was a single database.” But that sounds (to me anyway) like the first, more procedural option. And if you need a significant amount of cross data store analysis, why add another technology and complexities to do what could done more simply and performantly in a single database?

A reader asks: how about we design such that two sister microservices (A and B) use a child microservice (X) that offers an API to one shared persistent data store? Surely Jeff Bezos might say: If A (via X) reads persistent data created and maintained by B (via X), that is a “dirty read”? And Martin Fowler would say: If A and B update the same data, they share ownership of it, and are coupled by dependence on one database schema? E.g. If A needs a telephone number added to the customer table, either B will have to know about it, or via a succession of such changes, the supposedly shared database API offered by X will become one for A and one for B.)

Lessons?

There is no silver bullet, all is trade offs, you can’t have everything. Discussion of middleware technologies, cloud computing and fancy design patterns can obscure the need to remember the following.

-1- Understand the data processing requirements

-2- Keep the design simple, consistent with known and predictable requirements. Craig Larman warned that decoupling now for future requirements that may never arise is time not well spent. Why complicate the design, deployment and management of an application to meet requirements (extremely high throughput, volatile scalability, or reuse of application components in other applications) that are not real?

-3- As Fowler suggests, using “lightweight” communication mechanisms can be fine, especially between small components. Message brokers and workflow engines have their place, but not all messaging needs to go via heavyweight middleware.

-4- As Fowler’s principle of “smart end points, dumb pipes” suggests, business rules are not best placed in messaging middleware. Business rules engines have their place, but some rules are best coded near the user: e.g. email address must include the @ symbol, or a list of valid country codes. And some rules are best coded as near to the stored data as possible: e.g. referential integrity rules and other data quality rules. (Is coding rules on the data server really so bad?)

-5- The microservices architectural style does not dictate the software design pattern to be used within one microservice. A “rich domain” design pattern may suit complex applications, but for others, transaction scripts (which can share subroutines) can be fine. Basing microservices on transactions can make it easier to optimize the scaling of an application to handle different transactions with different (possibly extreme) throughput rates.

Further reading and technology-specifics

Some of this material appears in our architect training courses. For more discussion along the same lines try the older version of this article https://bit.ly/2NAZPfm

There follow some nodes from Linked in on technologies.

From Docker to Serverless

https://www.serverless.com/blog/why-we-switched-from-docker-to-serverless

Motivations for Kubernetes

What percent of enterprise applications really need to scale beyond what a monolithic application can handle? People talk of deploying scores or hundreds of containers, and needing Kubernetes to manage the complexity. This source on Kubernetes says:

  • “Running containers in production is not a picnic or a funny thing. It requires a lot of effort and computing; it requires you to solve problems such as fault tolerance, elastic scaling, rolling deployment, and service discovery. This is where the need for an orchestrator like Kubernetes comes in. There are other orchestration platforms, but it’s Kubernetes that has gained enormous traction and the support of major cloud providers.
  • Kubernetes, containerization, and the micro-services trend introduce new security challenges. The fact that Kubernetes pods can be easily spun up across all infrastructure classes leads by default to a lot more internal traffic between pods. This also means a security concern, and the attack surface for Kubernetes is usually larger. Also, the highly dynamic and ephemeral environment of Kubernetes does not blend well with legacy security tools.”

The same sources lists motivations that may be questioned.

  1. The need for a running environment, always. Isn’t availability a feature of the cloud in general? IBM mainframes reportedly deliver 99.999% availability, more than enough for most enterprise apps.
  2. Better resource utilization. You mean you’re managing cloud costs by scaling each microservice’s resources down the minimum it needs? Increasing the complexity of application management? Increasing response time?
  3. Vendor agnostic. Isn’t REST vendor agnostic?
  4. Branch integration with other products/branches. Do you mean (direct or copy) reuse of microservice X from application A inside application B? Is that commonly wanted and practical, bearing mind X may depend on other microservices inside application A?
  5. Taking containerized products to production. OK the development environment must be as like to production as possible. How containerized must an application be for using Kubernetes to be significantly better than using Docker?
  6. Auto scaling? Do most enterprise apps need scaling? Again, are you really trying to manage cloud costs by paring each microservice’s resources down to the minimum? And if you want optimal scalability, might you do better to code each transaction script in its own container?

There are three types of autoscaling. Cluster autoscaling adjusts your cluster’s number of worker nodes to optimize your nodes’ resources. Horizontal pod autoscaling adjusts the number of pods in your deployment based on the pods’ CPU and memory utilization. Vertical pod autoscaling adjusts the CPU and memory of your pods to meet the application’s real usage.

Note on service mesh v API

Quoted from this Linkedin post

“In a microservices architecture, apps trade the rigidity and stability of the call stack for the flexibility and chaos of the network. Concerns such as latency, outage retries, security, and traceability that were not a concern with a call stack become a concern with a service call. Service mesh is a pattern that has arisen to take these concerns out of the hands of coders so that they can stay focused on coding business solutions.

  • … use a service mesh to manage, secure, and monitor their services. The traffic between intra-application services is what a service mesh is best suited for.
  • API gateways should, in contrast, be used to manage interactions between your business and your partners or between one internal business unit and another.

A service mesh comes in a variety of patterns, but the ideal pattern you should utilize is a sidecar proxy running in containers. Although a service mesh overlaps heavily with API management, security, resilience, and monitoring, it is best viewed as a cloud technology since it is so intertwined with containers and is meant to support cloud-native apps — the apps designed to run on public cloud and also private (on-premises) cloud containers.”

References

This article was written originally by Graham Berrisford at LinkedIn, who has given written consent to re-post it here. All credits belong to Graham.

--

--

David Garcia
David Garcia

Written by David Garcia

Senior Software Engineer, Backend, NodeJS & Symfony developer, workaholic, passionate for new technologies and OSS contributor. https://linktr.ee/davidgarciacat

No responses yet