Have we gone too far with Microservices?

Jeff Barczewski
Jeff Barczewski |

Microservices has been promoted as a key architectural pattern in software engineering for the past 20 years. Looking back at how we’ve implemented microservices over the years, have we gone too far with this concept? Is our use of the microservice pattern facilitating the efficient delivery of software or hindering us? This article takes a brief look at some of the key goals of Microservices, how they have actually affected our teams, and some ideas for evolution going forward.

Brief Microservices History

Resource-oriented computing was a concept that Peter Rodgers was promoting at Hewlett Packard Labs in 1999. Eventually this research led to REST, Software Components, and Micro-Web-Services. These Micro-Services are loosely coupled services abstracted behind URI interfaces. Since they are independent, they can utilize different programming languages and be scaled separately.1 Alistair Cockburn described the hexagonal architecture pattern in 2005 which separated the user interface from the business logic. The modern microservices pattern often applies many of Alistair’s ideas.2

Modern Microservice Architectural Pattern

While the exact definition of microservices and their usage as a pattern has varied from company to company, they will often have many similar characteristics 1:

  • Processes communicate over a network (often using protocols like HTTP/S)
  • Services are organized around business domains
  • Services can be implemented with various programming languages, data storage, hardware and software environments
  • Services are small in size, bounded by contexts, independently developed and deployable


Key benefits commonly attributed to Microservices

Some of the key benefits commonly attributed to Microservices are 2,3:

  • Scalability - Individual services can be scaled independently based on their specific needs, allowing for efficient resource allocation.
  • Faster development - Smaller teams can focus on developing and deploying specific microservices, leading to quicker development cycles.
  • Fault isolation - If one microservice fails, it won’t necessarily impact the entire application, as other services can continue functioning.
  • Technology diversity - Different programming languages and technologies can be used within different microservices, allowing teams to choose the best tools for each task.
  • Continuous delivery - New features and updates can be deployed to individual microservices without requiring a full application redeployment.
  • Improved maintainability - Smaller, focused codebases within each microservice are generally easier to understand and maintain.
  • Agility - Teams can quickly adapt and iterate on specific features within a microservice without impacting the entire system.

Of all of these benefits, the underlying problem that sparked many companies to use microservices initially is “agility” (ie. freeing up development teams from stepping on each other in building their individual features). This continues to be one of the strongest benefits since many of the others can be achieved in a variety of alternative ways.


Unexpected consequences with Microservice implementations

What can we learn from several decades of deploying microservices?

While most organizations have experienced many of the proposed benefits, there can also be some unintended consequences from how microservices have been implemented in practice 4,5:

  • Deploying and managing microservices can be difficult unless your company already has good infrastructure and CI (continuous integration) tools setup that can be utilized. For a new company it may take considerable effort to create the necessary tooling for building, deploying, managing, and monitoring microservices. Should startups be spending time building this infrastructure and tooling first rather than focusing on creating their minimum viable products? Are there dedicated team members available to build out and monitor these services or will these roles fall to the individual team members as well?
  • Integration testing is hard, potentially requiring you to run local versions of all the services you use
  • Following a granular microservice approach can lead to an excessive number of services to maintain. Many companies may follow the philosophy that every microservice should do only one thing and this can result in having many small services creating many challenges:
    • Security vulnerability remediation - With a large number of independent repositories for each service, security vulnerability remediation can become excessively time consuming. The desire for more secure code has resulted in high levels of security vulnerabilities being detected each day. As the knowledge of how systems are exploited grows, so does the number of vulnerabilities for many of our dependencies. Resolving, testing, and deploying these for a large number of repositories can grind progress to a halt.
    • Debugging an application that spans many microservices is difficult - Unless your company has some elaborate distributed tracing mechanisms implemented across your services, determining what failed in an application can be difficult. Failures can often result in multiple teams being paged for critical bugs since it isn’t always obvious where the root cause came from. It may take many teams to collaborate by reviewing their own logs and metrics to determine who is actually at fault and how to come up with a resolution.
    • Evolving services - Services need to evolve over time, providing or utilizing new data needed that was unanticipated in the original design. Modifying data or data types could cause the contracts between services to need revision. Breaking changes may require API versioning and its ballooning support or alternatively, coordinated changes across services, all of which need to be deployed together. If these changes involve multiple teams, progress may grind to a halt while you navigate the priorities of the external team’s backlog.
      • So, while “agility” is one of the main reasons many companies choose to use microservices, evolution of fine-grained microservices can be somewhat challenging.
    • Data consistency - It may be desirable to update data in multiple services at the same time (like with a transaction where it all goes in or nothing) which may be hard if the data is in separate databases or hidden behind a microservice API.
  • Routing and security - Achieving the proper security and access across a set of microservices can be tricky. Firewalls, VPC’s, routing restrictions, and security permissions have to be implemented to protect your resources while still allowing the necessary access for operation. Diagnosing problems, especially with new systems or due to network changes, can be very painful. Troubleshooting may require checking/testing access at each network hop or service boundary.
  • Latency - Requesting data across a network can add orders of magnitude more latency than a direct function call. The problem is only compounded if many services are involved.
  • ETL and joining data - If data governed by a microservice is hidden behind an API, it may make the process of joining data or ETL (Extract, Transform, Load) much more difficult. For example, Polars, the Rust and Python DataFrame library, can be used for efficient ETL operations by streaming and combining data from many databases or cloud storage (parquet, csv, json) on the fly. However, consuming the custom API’s of a microservice would likely require a fair amount of manual code to perform ETL operations. 6

What can we learn from all of this?

  • If our primary goal is simply to promote “agility” and not have teams blocking each other, then there are many ways to achieve this even without microservices. If your team has its own repository, then simply having the code structured in well-defined modules will promote the ability to work on features in a parallel fashion. You may occasionally need to merge branches together for deployment, but if most features affect different modules/files, this is very manageable. Additionally, new tools like GitButler allow your team to work on simultaneous feature branches which are easily merged for deployment.
  • So rather than going to full microservices, consider modular miniservices, modular macroservices, or even modular monoliths.7 Having fewer repositories and services will help you with:
    • deployment – simply less things to deploy
    • vulnerability remediation - fix a particular vulnerability fewer times
    • debugging - call stack errors and logging are easier to follow. You might even be able to use a debugger on the process locally to help find the problem. Well defined modular code is easy to follow and step through. Teams will often have an intuition about where to start looking since there is one codebase to search through.
    • integration testing – generally easier on a consolidated codebase especially if dependency injection is used. Inject test versions for storage API’s that use a local database or file system, and the remaining logic can often be tested unchanged.
    • evolving data - contracts between modules in your codebase can easily be resolved with minimal coordination inside your team. Type safe languages like Rust can help you ensure all the modifications have been completed in the code base.
    • data consistency - if you are fortunate enough to be using the same database, then transactions will help you ensure data is updated together or not at all.
    • routing and security – reduced external endpoints to secure, inter-module calls will now be simplified to internal functional calls
    • latency - direct function calls are orders of magnitude faster and generally if your process is running, then all of your modules are available. Direct functional call efficiency may give you performance improvements allowing you to run on considerably less hardware.
    • ETL and joins - having direct access to databases or cloud storage rather than through custom API’s makes ETL with a tool like Polars6 very simple.
    • Startups/Proof of Concepts - startups and teams trying to quickly prove out a concept, will find modular monoliths accelerate throughput. At these early stages the contracts between modules change quickly, but these refactors are easy to make and deploy with a single codebase. Once your code has matured, you can still break apart modules if that is needed for scale or logistical reasons.

  • Let your own team dynamics and development patterns help you evolve into the ideal structure and architectural patterns. If you begin with modular code, you can always change how it is deployed later. It could easily be split into its own service if needed. It’s often very beneficial to be able to delay decisions till later and having modular code gives you great flexibility.
  • Technology can still be mixed or polyglot.
    • For instance, Rust can also create fast, safe, multithreaded Python or Node.js libraries (packages) and in doing so you may be able to dramatically improve the efficiency, performance, safety, and security of critical modules.
    • Alternatively, Rust and other languages may also compile to WASM, which allows modules to be run cross-platform from many languages.
    • Use discernment when introducing additional languages since it could introduce complexity and shrink your contributors to those who are willing to learn a new language. Your team dynamics need to be carefully taken into account for a polyglot solution to succeed.

Conclusion

Reflecting on everything we’ve touched on I’d like to leave you with these key points:

  • By taking a fresh look at architectural patterns that embrace a modular codebase, you may be able to dramatically improve your team velocity and maintainability. Seek out experience to understand the pros and cons of each pattern.
  • Organize your codebase in modules and use dependency injection to delay many decisions till later. Once you have some operational wisdom behind you, these decisions will often become straight forward for your needs.
  • If your team does not have the complexity of a Google or Amazon, then you may never need a fine grained microservice approach. Start simple and evolve over time with a clear understanding of the pain you are trying to relieve.
  • Consider alternative approaches before jumping to the microservice magic bullet; there’s often a simpler solution that could be an even better fit.

If you would like help with these decisions or in setting your team up for success, reach out to us at Sketch Development Services, we would be happy to help.

 

Development

Keep reading