This is going to be a short post, with the idea of being an intro for a future series of more extended ones if it seems interesting and if I get some time.
I wrote a post weeks ago sharing some thoughts about serverless architectures and their tradeoffs. I had a previous bias on the use of Serverless solutions (specifically on serverless functions).
I was thinking of them just as a way to trigger simple tasks without caring too much about managing resources, but I’d never thought of building an entire system based on them. Things and thoughts change, and now I see a big advantage in them. But serverless architectures and the frameworks that allow working with them don’t tell anything about how to build stuff; they just provide a way to build and deploy easily the systems. Without a proper architectural design, things can get really messy, and the advantages of serverless functions could turn into cons.
At my current job, we’re having some fun refactoring part of our core codebase. With several years of tech debt accumulated, tech leadership changes of thought, and employee turnovers, some of the services have developed an (ironic) interesting coupling to the serverless way of thinking, which has turned into real issues:
- Serverless functions can be directly invoked inside another serverless function, instantaneously coupling lambdas.
- For some time, AWS lambdas could not be triggered through message queueing systems (like SQS). Trying to follow an event-driven approach on a Serverless-based microservice mesh could be a nightmare.
- The ephemeral existence of serverless containers (they execute their duty and close the process) can lead to consistency issues if async processes are not really well handled.
Yep, this is giving us some hard time while refactoring things.
But, the solution to our problem was quick to find and is quite simple to understand (or at least easy now that we know the problem we are handling). It is not as simple to implement, but we’re doing it little by little.
Classic DDD strategic patterns basically told us to drop out the idea of serverless while we’re modeling our system. We started rethinking about our current domain, what the contexts related to the business areas of the company are, what we need to implement, and, after that, expose that through serverless.
With inspiration from the good old classic hexagonal architecture, we’re starting to rebuild things without caring about how the entrypoints are. Some conclusions that we arrived at were:
- Treat serverless solutions as another framework and just avoid coupling to them.
- Think of each lambda as a controller or as an event handler that will call internal layers, just as another part of the infrastructure layer. Not caring about which type of trigger will call our services, just defining contracts to use them.
- Build the app from the domain to the external layers even if it’s a simple solution. Once done that, you won’t care if the entrypoints are done via Lambda, Azure function, or via ExpressJS routes, and they will scale easily.
About the internal calls to other lambdas… we’re refactoring them to be more event-driven. Or, for tricky cases, wrapping adapters over them (we’re having a lot of conversations about domain modeling lately in parallel).
And, for the major architecture… Abstract also to the idea that services are run serverless (keeping an eye on performance/cold starts and similar, though). Build services as if they’re another microservice.
Other issues that we’re having related to monitoring or latency are a tale for another day.