Microservices versus monolith: The debate has raged for about a decade. Sure, there are pros and cons for both architectural styles, and best practices that can make either run efficiently regardless of the software application. But I’m not here to pick sides. I want to talk about what happens if we combine some of the best parts of both. Why choose between the easy maintainability of a microservice or the unified source of truth inherent in monolithic applications if you don’t have to?
This simplicity can reduce friction in workflows, deployment, and, hopefully, the dreaded “it works fine on my machine” problem.
Imagine a world where production and development are one and the same. (I know, I know, but indulge me, please.) When I say “the same,” I mean that the line between them is not defined by a CI/CD pipeline or the PR two-step, but by just pushing to master and having code exist in an AWS/Azure/GCP function—in production. This simplicity can reduce friction in workflows, deployment, and, hopefully, the dreaded “it works fine on my machine” problem. How do we get to this sacred land? By adopting what I call nanoservices.
What is a nanoservice? It’s a piece of code, usually a method or function, that can be deployed to production without the need for scaffolding or boilerplate code. An engineer would encapsulate her business logic in that function and deploy it using familiar patterns: commit via git, push to master, trigger CI pipeline, etc. The key difference is that this piece of code is a singular serverless microservice in your overall application. I call it a nanoservice because multiple functions make up the engineer’s overall business logic, and this business logic encapsulates a microservice. (The same approach can be used in a monolith, but then it’s no longer a monolith—it’s a giant microservice.)
Let’s say the engineer is building the checkout flow for an e-commerce app. That flow can be broken down into multiple functions:
Verifying the items in the basket are available for purchase.
Fetching the buyer’s payment details.
Communicating with the buyer’s payment processor API.
Sending a confirmation email to the buyer.
Interacting with the database to execute the above workflow (updating quantity of available items, creating new details for the buyer, etc.).
Each of these functions will exist as stand-alone serverless functions. Put together, these create the checkout feature.
Many software applications are necessarily becoming easier to use. But that doesn’t mean it’s simple to create them.
As computing becomes ever more inextricable from our daily lives, many software applications are necessarily becoming easier to use. But that doesn’t mean it’s simple to create them. In fact, the simpler an application feels to use, the more likely it is that the burden of difficulty has shifted to the engineer and is represented in its architecture. Nanoservices can help ease the burden due to its particular advantages:
Security: A nanoservice can have its own security protocol. This brings a level of granularity to an application’s security layer that was previously reserved for the server and application level. This can help separate the services that handle sensitive data from the more generic services.
Pure encapsulation: A nanoservice can house only one function. That function houses a piece of business logic. And that nanoservice can be called from anywhere within the host’s infrastructure, provided the requesting infra has permission to execute it.
Feedback loop: Your dev environment can be one with production, with constant feedback around how your nanoservice performs in the wild. A nanoservice will collect information and metrics just as a regular microservice would, allowing for extensive metrics monitoring and tooling.
Why do I see this movement toward higher distribution and granularity? Because I’ve noticed a shift, particularly in larger software teams, toward single responsible teams—teams that are exclusively responsible for a group of features that makes up a product line for a business. Within those teams, I see people writing code that could be used by other teams—but because they are relatively siloed, code tends to be unwittingly duplicated. That duplicated code may then be duplicated somewhere else, and somewhere else again, with only cosmetic differences (e.g., method name, variable names), which can bloat the overall business application and may confuse engineers who are newer to the codebase.
With nanoservices, engineers can create business logic once and then encapsulate it as a serverless function, allowing it to be reused. Alas, the road to the promised land is paved with obstacles. Every architectural decision comes with trade-offs. Nanoservices, for all their benefits, are no different:
Cost: Serverless infra can get expensive when it’s under the same load as traditional application infrastructure. (Einar Egilsson sums up the situation in his blog post “Serverless: 15% Slower and 8x More Expensive.”)
Latency: If latency is a big concern, then nanoservices may pose a problem. The combination of a cold-start serverless package and a nanoservice calling another nanoservice could potentially cause an increase in latency within your application.
Resiliency: One disadvantage of microservices is that callers/callees have to handle network errors, rate limits, and circuit breaking, among other things. This can result in dropped requests and increased latency. However, creating a nanoservice “error handler”—one nanoservice that handles errors in a configurable but still standardized way (e.g., changing the sleep period for a rate limit, while the sleep function is executed for any rate limit error)—is one way to bolster the resiliency of a nanoservice architecture.
These disadvantages have little to do with the engineering and more to do with the cloud provider. They should become easier to surmount with future iterations on serverless offerings.
The best architecture isn’t the one that just ticks all the boxes in the tech spec, but one that elicits discussion and deeper understanding around what is being built.
Architecture is like art: It’s the result of engineers’ experience, skill, creativity, and interpretation of how their product will be seen or used in the outside world and how they can help facilitate that use. The best architecture isn’t the one that just ticks all the boxes in the tech spec, but one that elicits discussion and deeper understanding around what is being built.
The beauty of any work of art is in the eye of the beholder (and maintainer). For me, that’s granular, concise, and reusable components that keep an application’s business logic loosely coupled with the overall architecture, while allowing the nanoservices to be used as building blocks for other, larger services. Such an architecture can free up engineers to create the most epic of applications. Operating within it, single responsible teams should be able to understand the nanoservice developed by another team and utilize it accordingly.
Nanoservice architecture may not be ready for prime time just yet, and it won’t be until the serverless prices and response times are drastically reduced—and until organizations are willing to adopt this change in style and workflow. But in my opinion, small and reusable components will materialize in some form. I’m excited for when that day comes. Long live nanoservices!