When building a complex system, it’s often really hard to hold all the elements of that system in your head. Then things get added to it, and added to it, and after a while, the application is too much for one person to understand except in the broad and abstract “it does this”.
The biggest serverless systems I have seen and the ones I’ve built seem to exhibit this behaviour more quickly. It’s not that there is more complexity in the overall system, but that the pieces are simply less coupled, less connected, and this is by design. A few functions connecting to more managed services than is normal in a non-serverless application leads to great value in areas such as resilience, and long term maintenance, but can be more complex to understand as a whole.
A couple of days ago, esteemed journalist Bill Thompson asked on Twitter what the name for a system so big and complex that we can’t grasp or imagine it is…
Turns out the word he was looking for was “hyperobject” and it’s a concept I’ve come across before, but not for a while. This piqued my interest though as it seemed a good word for how I look at serverless systems.
The idea of a hyperobject is as Bill says, an attempt to describe a system that is so complex that it cannot be fully understood. The original idea is explained on Timothy Morton’s wikipedia page and is probably as useful overview as any (although it has its critics, and understandably so).
I tend to look at the idea of a hyperobject as if it is something similar to the story of the blind men and an elephant. While they all can touch a part of it, they only see partially, and cannot understand the concept without collaboration, and even then how does one explain their understanding to the each of the others? It is not an easy thing to do to explain the feeling and touch of an ear to someone who is touching a tail or a tusk.
Hyperobjects and software
When we build applications and software, there is a great emphasis on trying to understand the wider system. Technologists try to ensure that if you pick up one piece of a system it should be “understandable”. This is largely because software often is a part of a whole, a small piece in a package.
When you build a serverless system, the pieces of code are often small and self-contained. Often these pieces are much more loosely coupled to the rest of the system, so the code does not need to relate to the rest of the system, but the piece does. So the relative understanding changes. “What relates to what?” changes.
The relation of pieces matters because it allows the overall application to change more rapidly. Adding onto a more loosely coupled system is a lot easier than a tightly coupled piece of software. This is often seen as a bad thing by people who want to control the boundaries of the application. But largely (although not completely) those ideas of boundaries spring from the tight coupling of code, and the consequences of allowing bad code to have access to the wrong elements of an application.
There’s another way of looking at the relation of pieces and the ability to change though.
The Flexible System
One of the holy grails of the technology world is flexibility of systems. The ability to upgrade an application, change things in the future, make things easier to build upon and modify over time. The strange thing is that most people will never tell you about this and will never really think about this.
When building on the cloud services, and using single-purpose functions and events as a way of developing a loosely coupled application, you can end up with a very flexible, easily adaptable system. It can scale up fast, scale down fast, be updated rapidly and will likely cost significantly less in both actual monthly cloud services costs and operational expense in maintenance (so long as you use IaC and CI/CD).
This flexibility is significant. A team can be given a new feature and there is much less of a need to “undo” other features, or ensure other code is updated to accept it. This is the beauty of a loosely coupled system in this form. There are drawbacks (it can take a little longer to build), but if you build a set of working principles, this can be highly valuable.
Where most people have thought about flexibility, in some ways, is in the concepts of “Frameworks”. They will use an open source framework to provide this ability to adapt, and use the argument that there is a community of people out there working to improve it, and if the application needs additional functionality, then it can be added to over time.
This is a fascinating argument. Most (not all… most) of the frameworks I’ve seen have never been upgraded by anyone other than the original creators of the framework. These frameworks often provide a lot of value, but they often tend to have some sort of “plugin” mechanism in the end, where a developer takes the “core” and then can add to it. This is how the framework becomes “flexible”.
Except it isn’t. Because it would be exceptionally hard to move away from the framework.
And for many applications, this is a perfectly acceptable and reasonable trade off to make. Do it! It’s really adequate for the needs of that application and should be done.
But don’t make the mistake of believing that it provides you with future flexibility. The framework route is the route of future legacy and always will be.
And, to be clear, if you build on cloud services as you do with serverless, you are relying on those cloud services, and so you are choosing a different trade off. But it is the better long term play by far.
Serverless, Principles and Hyperobjects
I still get told that it’s just “easier” to go with an EC2 server or Heroku and put ruby on rails or django on it to build an MVP. I am more and more skeptical of this approach the more I see the value in serverless applications. The idea of a throwaway first version seems far less needed now we have serverless.
And this is because I’ve already done it. In 2015.
What I’m talking about in this whole post is not abstract, but from experience. One of the most difficult things when we built the backend of Movivo in a serverless way with AWS Lambda and DynamoDB in 2015 was that it was very hard to understand the whole system.
Our back end was, in effect, a hyperobject.
We could see the repository. It had the code and the infrastructure, and all the ways it was joined together.
But we had no “picture” in our minds of what it meant. There was no diagram that explained it.
We could draw parts of the application.
We could imagine how pieces joined together.
The language of microservices didn’t really work for us, because we hadn’t built it like that, for lots of good reasons.
The language of servers was unhelpful because we didn’t really have servers in there.
We didn’t have a language to explain it, so we made our own, and it’s adapted over the years for me too.
Now I talk about “flows” of information and “paths” through the application. I find it difficult to talk about a “service” in this context because when we built things there wasn’t really a service boundary to talk about. There could have been, but because we built in “flows” using our events as triggers to do multiple different things, we had to define a whole different set of ideas.
So we had to define principles for developing our system. We realised that if we didn’t define principles our system would become unwieldy very fast.
One of those principles was a simple one:
You worked backwards: Each element of the system had to fully explain itself if there was an error.
Instead of seeing errors in logs, and then diving into the stack and seeing where the error was, going to that line of code, and seeing what happened and … you get the picture, the ideas of “working backwards” was that any code would deliver pinpoint detail about the exact issue. So it relied on a good naming convention for functions “AddToUserTable” and any error had to be caught and a human readable message appear “Adding a user failed because …”. That way, if we saw an error appear, it was immediately clear where the error was, and why it occurred.
Another principle was that anyone needed to be able to fix any issue quickly. Which was why the errors had to be so clear and the CI/CD process had to be as fast as possible.
This made our loosely coupled serverless system very clear and easy to maintain.
This easy maintenance meant that more time was available for building features.
But that also meant our system grew very fast.
Eventually, with this kind of problem, you simply give up trying to control the architecture.
So long as you are aware what the errors are fast and can fix them, and you are building the correct features, and you have some way of monitoring whether the application is delivering what it is supposed to be delivering (important!) then your application works.
And ours was a hyperobject.
In some ways at least.
The difference between this application and others I have built and seen is that it was very much more flexible and malleable than almost any other I had seen. Bottlenecks occurred, but could be mitigated far faster. Code could be improved faster because the system was so loosely coupled. New features were simple. And it was over time impossible to visualise as a whole.
Building complex systems is not easy
Technology is never simple. I’ve seen complex systems built in numerous ways, either using servers in data centres, virtualisation, containers, kubernetes, and fully serverless with all the managed services you can imagine and all things in between.
Nothing is ever perfect (not even serverless) but the one thing I can say is that the biggest headache is not the initial build. It never is. That’s only the first headache (and I’m not diminishing it). The biggest headache is managing a large unwieldy stack once you have committed to it.
The most important element over the medium to long term is simply flexibility. The ability to change and add and update and limit the time spent on maintenance is something that a lot of developers don’t even think about. The response of “It’s what I know” is never good enough in my view to the question of “How should we build this?”
Too many times, I’ve heard the response “I can’t visualise the system” or “I don’t know what it looks like” when I talk about serverless applications. I genuinely find this frustrating because most people couldn’t tell me what their applications really look like. What they mean is that they can’t visualise the “architecture”, because they are so used to putting code on a “server” and having some form of data layer (or layers) that they don’t see the actual software application as something that needs visualising, when in fact it is just as complex, if not more so than many serverless systems.
The point is that grasping the lack of ability to visual and understand a system is key to being able to change the mindset of how you approach software and complex system development. At that point, Mean Time To Recovery (MTTR) all of a sudden becomes an absolutely top priority, and everything follows from that (note: JUST READ ACCELERATE).
I go on about “serverless” but…
…it’s not really about Function as a Service or any of the technologies.
What I’m really going on about to most of the people in tech is:
STOP. WASTING. YOUR. TIME.
This was almost the entire reason I picked AWS Lambda for my back end in 2015 (along with the idea that it might lower the application carbon footprint — pretty sure it does).
And if something better came along, then I’d advocate for that.
But I see so much waste that happens under the banner of “it’s what everybody else does” and it’s so frustrating.
So pushing serverless ideas and talking about hyperobjects and trying to get you to think about something other than technology is me trying to tell you to stop using up energy on things that don’t matter, and use it for the things in life that do.
Like family and friends and hobbies and stuff like that.
Because 2020 has shown us that the tech world is really not that big of a deal in the end. There’s more to life than this.