Microservices vs. Monoliths

John Freeman
Sep 9, 2018 · 17 min read

TL;DR

Microservices are a better fit for the priorities and resources of large companies and governments.
Microservices allow you more freedom to increase resources and so reduce your time to market. They can scale to meet the most demanding performance requirements. They can also extend the life of your product through a process of gradual evolution.
Applications with highly coupled services are more suited to a monolithic architecture. As are applications with a large interface between their back-end and front-end.
The overhead of a microservice architecture increases the costs for training, development, testing and production. These costs are proportionally higher for small to medium size applications. There’s a large amount of setup needed whether your microservice application serves 10 customers or 10 million.
However, there’s a limit to how much you can effectively scale development-resources and performance with a monolithic application. When you need to overcome that limitation, microservices deserve serious consideration.

Advantages of Microservices over Monoliths

General business benefits

  • Greater independence between teams
    The more teams can work independently the more they can work effectively on one or more related applications. This is what makes microservices such an effective solution for large companies and governments.
    In practice, many projects never achieve enough independence between teams. You need a problem that splits up well into independent microservices (many don’t, particularly those that need large user interfaces). You also need to build your microservices in such a way that you minimize dependencies between microservices and teams.
  • Greater independence between features in development
    Features are typically worked on and released independently. A delayed feature no longer needs to hold up a whole release. Small features can be released without waiting for the completion of large features.
  • Improved performance scalability
    If you have the performance demands of Netflix, Amazon, Twitter, Google etc. microservices can scale to meet your needs.
  • Faster release cadence
    Ideally, each merged commit should be tested and released to production. This simplifies versioning, you always develop and test against the production version of your microservice dependencies.
    Be aware, it may not be possible for the front-end to keep up with the release cadence of the back-end.
    Users may not be comfortable with frequent front-end changes. Feature flags can be used to hold back user interface changes if necessary.
  • Smaller, lower risk releases
    Releasing one merged commit at a time makes for small low-risk releases. These small releases are also easier to rollback if necessary. This takes significant investment in continuous integration and continuous delivery pipelines. Manually testing and deploying microservices requires too much effort.
  • 24/7 availability
    It’s not practical to take the application down for each deployment, so you also have to invest in zero downtime deployments. This coupled with the redundancy, resilience, health checks and container management, should mean your application stays up 24/7.
  • Easier to track development progress
    There’s little option than to test and release as you go. You end up with a stream of completed tasks rather than a vague estimate of how much the whole is completed.
  • Easier to sell parts of an application
    If you build your microservices to be as independent as possible it’s easier to drop them into another application.
    In practice, there’s a tendency to build microservices that are tightly-coupled to the application they’re written for. Minimizing dependencies between microservices should be a major focus of your design and training.
  • Easier to replace parts of an application
    It’s much easier to overhaul individual features and to integrate third-party products. You have a clear remote interface so the implementation can be swapped out without affecting the rest of the application. You can even use an implementation with a completely different technology stack. Unfortunately, if you need to make changes to the remote API (and impacted microservices) then it can get much more costly.
  • Better fit for an agile approach
    Large features typically consist of multiple API endpoints across multiple microservices. This usually breaks down into small manageable tasks verified by integration tests.
  • All the cool kids are using it
    Brag to prospective customers and employees about how you’re one of the silicon valley cool kids too. Find somewhere to use blockchain as well and you’ll be partying with Mark Zuckerberg in no time. Just hurry before Microservices goes the same way as CORBA, EJB, SOAP, ESB, etc. :-)

Technical business benefits

  • Increased resilience to partial failure
    In theory failure of one microservice can be contained so it doesn’t affect the rest of the application. In practice containing failure requires careful design, implementation and a lot of effort. Applications using Microservices have many more potential points of failure so are unreliable by default. This encourages you to design for unreliability to get the overall reliability you need.
  • Small easy to develop code bases (for individual microservices)
    It takes far less time for a developer to get up to speed on a microservice than a large monolithic application.
  • Fast build times (for individual microservices)
    The less code in a build the faster the build. The less time your developers spend waiting for their work to build, the more time they spend writing code that’ll make you money.
  • Allows you to move with technology (without requiring a total application rewrite)
    Microservices should be small enough that they are relatively quick and cheap to rewrite. This allows you to migrate to newer technologies and approaches one microservice at a time. You’re no longer stuck with the choices made at the start of a project because change is too big and too risky.
  • Allows for a greater mix of technologies
    Each microservice can make the best technology choices for the problem it’s trying to solve. Don’t go crazy, you need enough skilled people to support, maintain and enhance your microservices.
  • Quicker CI builds (for individual microservices)
    The longer the CI builds the more resources you need for your build servers and the more it costs.
  • Smaller, lower risk deployments
    No more white knuckling releases where your company’s reputation is riding on a massive quarterly release (that could use another month of testing). You no longer spend the 2 weeks post-deployment fixing the bugs your customers find.
    Every change should be tested and deployed individually. There’s less to go wrong. It’s easier to identify the cause of any problems because less has changed. Failures are likely to cause less customer impact than a large release.
  • Code consistency easier to maintain (within individual microservices)
    The less code there is the easier it is to maintain consistency.
  • Easier to use batch processing
    If your microservices are asynchronous and message based it’s relatively easy to use batch processing. Batch processing can provide significant performance benefits.
  • Easier to do canary testing
    You can run the current version and new version of a microservice simultaneously. You can limit what traffic goes to the new version until you’re confident it doesn’t cause any problems (see Istio).

Disadvantages of Microservices over Monoliths

General business issues

  • Developers are less able to move between teams
    Different microservices may use different technologies. Teams are largely unaware of the functional requirements and history of code on other teams with other microservices.
  • Remote APIs are more vulnerable to attack (than local method calls)
    Putting these APIs on the network makes them much easier to call. Developers have a bad habit of implementing security last (or not at all). Most developers are not security experts and are not familiar with all the potential attack vectors. The necessary security constraints may not all be obvious.
    You should perform security testing against all your remote APIs (even if they’re not exposed externally).
  • There’s a lot more tech needed
    To make a distributed system behave like a centralized system.
    To make microservices appear externally as a monolith.
    To manage configuration across microservices.
    To make deploying/running many microservices as easy as a monolith.
    To maintain security between microservices.
    To provide a single access point for support (e.g. log aggregation).
    Because repeating processes for each microservice necessitates automating these processes.
  • Training needed
    Few developers are familiar with all the patterns and new technology involved.
  • Technology changing fast
    You may have to adapt to significant changes in your chosen tech stack or replace technologies if they don’t survive the competition.
  • Increased hosting costs
    For most applications, RAM will have a bigger impact on cloud hosting costs than CPUs. Microservices (particularly Java microservices) tend to be less efficient in their use of RAM so are more costly than monoliths to host. That said, hosting costs are typically small compared to the cost of software development.

Architectural issues

  • Cross-microservice transactions (i.e. distributed transactions)
    Forget 2 Phase Commit / XA Transactions, it’s a blocking protocol that doesn’t scale and your varied tech stack isn’t likely to support it.
    You’re going to have to manually orchestrate the transactions, dealing with retries and rollbacks yourself. This typically involves using the Saga pattern to orchestrate the transaction. Compensating transactions are used to rollback transactions when necessary. Reliable messaging is used so the transaction is still completed/rolled-back even in the event of a system restart.
    You can also kiss goodbye to transaction isolation and atomicity across microservices.
  • Eventual consistency
    You may see a lag between when data is changed and when that change is visible. The lag may be different across different microservices.
    Changes may be strongly-consistent within a microservice, but they’re not between services.
    Eventual consistency can make it more difficult to perform data changes across multiple microservices. For example, a data change to microservice B may depend on data from microservice A that it hasn’t applied yet.
    Eventual consistency can make integration testing harder. There may be a lag between performing an action and being able to verify the resulting change.
    Event sourcing can be used to work around many of the problems caused by eventual consistency. Using event sourcing will normally lead you to use CQRS as well.
    Ideally, your microservices should demonstrate sequential consistency. Sequential consistency allows for fast pessimistic locking using event sourcing. Without sequential consistency, you have to introduce additional delays to provide confidence that consistency has been achieved.
  • Data joins across microservices
    We take SQL joins for granted, they’re easy to write and performant to run. Unfortunately, you can’t use SQL joins across microservices (at least without introducing a dependency between their database schemas). This leaves you with either data replication, performing the join in code in the back-end, or performing the join in the front-end.
    These joins need extra implementation effort, increase your backwards/forwards compatibility concerns, and need more integration testing. If your application has a lot of data joins across microservices, you may want to consider if microservices are the best fit for your application.
  • More potential points of failure
    You need to build assuming individual microservice nodes are unreliable.
  • API gateway needed to hide microservice boundaries
    An API gateway controls which APIs are accessible externally and routes external URLs to microservices.
  • Network security
    You have many more services exposing remote APIs it takes a lot of effort to ensure these are secure.
  • More configuration
    Most microservices will need configuration. This configuration may well be different in development from testing/production. The more communication between microservices the more configuration is needed. You may have a greater variety of technology to configure.
  • Many more remote APIs
    Remote APIs are more effort to change than local APIs. To reduce the need to make changes more effort needs to go into designing the API.
  • Asynchronous APIs
    APIs that change data or perform actions should be asynchronous / message based. This is necessary so you can use the Saga pattern for transactions.
  • Backwards and forwards compatibility issues
    In general, this means ignoring unsupported JSON properties and making new properties optional. Making a new property mandatory may be done in a later release. This affects calls between services as well as calls between microservices and the front-end.

Training issues

  • Too much tech
    It’s difficult for any one person to be expert in all the tech involved.
  • Unfamiliar patterns and frameworks
    Many of the patterns and frameworks will be unfamiliar to developers coming from a monolithic background.
  • Little coverage in university courses
    Perhaps universities will give more attention to microservice patterns in the future. However, providing practical experience of developing microservices would likely take too much time.
  • Less knowledge sharing between microservice teams
    The independence between teams is what allows you to scale development resources, the downside is less knowledge sharing.
  • Lack of good end to end documentation
    There’s so much tech involved, from a variety of providers, no one writes end-to-end tutorials that cover everything.
  • Not something you can cover in a 45-minute presentation
    There are plenty of good 45-minute presentations showing how to build a simple monolithic application. There are a lot of good presentations on specific microservice patterns and frameworks. You can’t explain the patterns, develop and deploy a microservice application (with all its dependencies) in 45 minutes.

Deployment issues

  • Different release cycle to the front-end
    If your microservice back-end has a different release cycle to your front-end application, this will create backwards compatibility issues.
  • Manual server configuration no longer an option
    Manually maintaining separate configuration files for each microservice across all environment levels isn’t practical. You’ll need to use a configuration server to hold your configuration.
  • Server provisioning
    Manually provisioning servers is no longer practical, there are too many. You’ll need to use some sort of container orchestration tool (e.g. Kubernetes) and DevOps practices to manage your servers.
  • Manual deployments no longer an option
    The effort of frequent manual deployments would be too costly. Frequent manual deployments also increase the risk of someone making a mistake. You’ll need to introduce continuous delivery tooling (e.g. Spinnaker).
  • More effort to deploy and keep running
    It takes a lot more effort to deploy several microservices and keep them running than a monolithic application. Automation will help but you still need to set up this automation. There are a lot more parts to go wrong in a microservice architecture. While you’d set up automated monitoring and alerting with a monolith there’s a lot more setup needed with microservices.

Runtime issues

  • Increased latency
    Network calls are costly in terms of latency. If you’re not careful microservices can end up with hundreds or thousands more network calls per request. Microservices are also likely to use message queues, which can also increase latency.
  • Many databases
    A separate database per microservice needs a lot of setup and a lot more backups.
  • Many message queues
    Each message queue will need setup and monitoring.
  • Log server
    You no longer have a single log file to look at. It’s not practical to log-on to different microservices to manually inspect their log files. You need to introduce a log server and use it to aggregate your log information.

Duplicate effort issues

  • Many source repositories to maintain & checkout
    Individually they’re not much effort, but once you scale this up to all your repositories across all your developers, this is a significant cost.
  • Many build scripts to write and maintain
    The company would rather you spent your time implementing features. Developers would rather be writing code. Few people know how to write them well. Often you start with a minimal build script and then it’s neglected as much as possible.
    How many build scripts are missing integration testing support, code coverage, style checking and static analysis? How many build scripts have out of date and duplicate dependencies?
    Now imagine you have 10 or 25 or 50 build scripts to write and maintain.
  • Many more CI/CD jobs
    Each will need configuration and occasional maintenance.
  • Vulnerable dependency versions need updating across many microservices
    Each time you find a vulnerable dependency you have to coordinate an effort across teams to update it.
  • Framework updates repeated across many microservices
    Many microservice frameworks are large with lots of dependencies (e.g. Spring Boot). Updating to new versions of these frameworks (and their dependencies) can be a significant amount of effort. This effort is then multiplied by all the microservices using the framework.
  • Duplicated code between microservices
    To maintain independence between microservices it’s often necessary to duplicate small amounts of code. Template projects may help to an extent but even these need effort to create and maintain.
  • Some fixes need applying to more than one microservice
    When you find a bug in your code you have to notify other teams so they can fix duplicate code in their microservices.
  • Duplicated configuration between microservices
    Most remote services need an address and authentication details configuration. You have a lot more remotes services in a microservice architecture, so there’s more configuration needed. Multiple microservices may use each service so there’ll be a degree of duplicate configuration between services.

Versioning issues

  • API changes take multiple commits
    You need a commit to the service providing the API; then a commit per microservice that needs to consume the API change; then you may need a final commit to remove the original API.
  • Versioning issues between microservices
    You don’t want a situation where your microservice change is dependent on future release versions of several other microservices.
    The best approach is to develop/test against the production version of all your dependent microservices. If you need a change to one of your dependent microservices you need to get this released to production before you can use it.
    The downside of this approach is you have to wait for your dependent microservices to be released. This is a good reason to use atomic versioning and automatically release every commit to production. This cuts down on the time you spend waiting for your dependencies to be released.
  • Versioning issues between microservices and the front-end
    Backwards and forwards compatibility between your back-end and your front-end is a major concern.
    When you need to make an API change on the back-end you’ll have to maintain backwards compatibility until the front-end is released.
    The problem is worse when you’re not in full control of the front-end version your users are using. You may have to maintain backwards compatibility with many versions of your front-end for an extended period of time. This is a testing nightmare, how many versions of the front-end do you test against? You have to maintain the tests for old front-end versions. You have to keep back-end code for backwards compatibility. It’s difficult to track which APIs are no longer needed by the front-end and can be removed.

Implementation issues

  • Immature tech stack
    A lot of the tech not inherited from monolithic applications is not yet mature. Immature tech is more subject to change, mergers or discontinuation.
  • Code consistency harder to maintain across microservices (i.e. across the application as a whole)
    This can make it harder for developers to move between teams and to apply fixes across microservices.
  • Tech debt accumulates on microservices not under active development
    The more your microservice approach improves over time, the more services not under active development fall behind. It may be necessary to make changes so deprecated APIs in other services can be removed. There’s also effort needed to keep up with the latest changes in your tech stack.
  • Asynchronous programming
    There are two aspects to asynchronous programming with microservices. The first is when you have an asynchronous remote API (e.g. a message based API). The second is using non-blocking asynchronous code for performance/scalability.
    Most microservice applications will need to have asynchronous remote APIs (e.g. so you can use the Saga pattern for transactions). Few need to adopt asynchronous code for performance.
    Asynchronous programming is typically boilerplate heavy and less intuitive than synchronous programming. For single operations, promises/futures have a manageable amount of boilerplate. The more operations you add, the more this boilerplate makes your code unreadable. This leads to reactive stream APIs such as RxJava, these use functional programming to handle a stream of operations more concisely. Reactive streams still need a fair amount of boilerplate and this functional style isn’t always as intuitive. So languages have added async/await and coroutines, to make asynchronous code more like synchronous code. Each approach has its strengths and weaknesses, and those are only some of the most common approaches.
    The bottom line is asynchronous code is more costly to write, so you should only use it where you need to.
  • Increased storage needed for release artifacts
    You’re going to have a lot of releases, which will need more storage space. This is particularly true of artifacts that contain their dependencies.
    Java developers should avoid uber-jars (e.g. using the Spring Boot plug-in); you’ll quickly max out your storage space and have to regularly delete old releases. You should prefer layered Docker images instead.
    You want to make sure your Docker images are split into separate layers for your dependencies and your application. Its important layers don’t change unnecessarily, so they can be reused. This involves techniques such as giving all files a fixed date. There are tools to help such as Google’s Jib (for Java applications).
  • Microservice project setup time
    People tend to overlook the time it takes to set up a new project. The repository, the build script, the build jobs etc.. With monoliths this is a rare event so doesn’t matter much. With microservices, this is a regular event and you need to account for the time and resources this takes.

Local development issues

  • Difficult to run locally
    An individual microservice with no runtime dependencies is easy to run. As soon as your microservice has multiple dependencies, or you want to run the whole application, it gets difficult quickly.
    There are too many microservices to configure, start and stop individually. You’ll need to use something like Docker Compose or Helm to help with that. Your local development machine may not have enough RAM, so you may have to run some/all services in the cloud. To fix bugs and performance issues across services, you’ll need to run a log server and performance monitoring tools.
  • Debugging doesn’t work across services
    With a monolithic application, you take for granted that your code debugger can step through all your back-end code. It’s relatively easy to track down the source of a problem.
    With microservices, you have to debug each microservice separately. As you follow the code across services, you may have to restart services to enable/disable debugging. You can’t just step across microservice boundaries, you need to add a breakpoint to intercept the request as it crosses over.
    This can make bug-fixing more time-consuming and costly. More logging and validation can help, but these cost to put in as well. It’s also important to avoid creating too much noise in the code or the logs.
  • Log information more difficult to follow
    It used to be pretty easy to trace the log information for a request by the thread ID and timestamp. Now asynchronous code means a request may be serviced by multiple threads. Microservices means it may also be serviced by multiple servers.
    You need to maintain distributed trace IDs to correlate the log information for each request (see OpenTracing). When viewing logs, you still need to be conscious that multiple servers are involved, and automatic retries may occur.
  • Poor IDE support
    IDEs generally treat microservices as separate projects, which means many features don’t work across microservices.
    With a monolithic application, I can run find usages on the `AccountService.getAccount()` class, and it’ll show me all the usages; you can’t do that on the REST endpoint on the Account microservice.
    With a monolithic application, I can refactor my API and the IDE will update all the code that uses it; you can’t do that with a REST API in a microservice.
    I can run/debug a monolithic application as a unit; you can’t do that with microservices.
  • Exceptions and stack-traces aren’t sent across services
    For security, you don’t want to expose internal exceptions and stack-traces to callers of a microservice. Doing so may expose a security vulnerability that can be remotely exploited.
    You either have to look in the log file of the other microservice, or use a log server during development. Either approach takes more time than if you had local access to the exception.

Testing issues

  • Needs higher test coverage
    You’re totally reliant on automated testing for your microservice releases. It’s not practical to manually test each microservice release.
  • Need to integration test across services
    At least some of your integration tests will involve multiple microservices.
  • A greater proportion of the tests need to be integration tests
    Code that was local on a monolith, and tested using a unit test, is now remote and needs to be integration tested. Integration tests take more effort to write and are slower to run.
  • Integration tests need cloud resources
    With a monolith, you may have been lucky enough to do all your integration testing on your CI server. But with microservices, you’re going to need to spin up a whole cloud-based integration test environment, to run all the microservices that are part of your tests.
  • Testing costs are higher with microservices
    Because you need to write more tests, the tests are more effort to write, and need more resources to run, expect to pay more for testing a microservice application.

Final thoughts

Microservices are a great solution if your application splits up well into independent services, has a small front-end interface, and requires many teams of developers to meet your implementation timescale. Applications with extreme performance demands may have few options but to adopt a microservice architecture.

If your application doesn’t have those qualities, the cost will likely far outweigh the benefit. You may be better off with a monolith or taking a more front-end centric approach to splitting up your application.

Even if you decide not to use microservices, you may find some of the technology useful, and the design patterns are applicable to integrations with third-party systems.

As always when choosing technology, clearly define the problems you are trying to solve, evaluate how the potential solutions solve those problems and at what cost.

Thanks to David Freeman

John Freeman

Written by

Software Consultant at GantSign Ltd.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade