Frog and Toad eating cookies. Illustration by Arnold Lobel.
One of my favorite Frog and Toad stories goes like this:
One day, Frog and Toad make cookies together. Later, as they are scarfing down cookie after delicious cookie, Frog suggests that if they don’t stop eating cookies, they will get sick. Toad agrees.
So they put the cookies in a box. But then Toad points out the obvious: they could easily open the box and eat the cookies. Frog responds by tying a string around the box. But then Toad objects that they could still cut the string and open the box. So Frog proceeds to fetch a step ladder and put the box on a high shelf. Toad counters that they might take the cookies back down, cut the string, open the box, and eat the cookies.
They realize they are at an impasse. All of the safeguards against eating more cookies are easily bypassed. Frog announces that what they really need is willpower. After explaining what willpower is to Toad, he takes the box down, cuts the string, opens it up, and offers all of the cookies to some nearby birds, who are happy to gobble them down.
Toad laments that now they have no cookies at all. Frog concedes this point, but points out that they now have lots and lots of willpower. Toad replies that Frog is welcome to keep all the willpower, and goes home to bake a cake.
New bikes and new programming languages
There’s a recurring discussion I see in software that reminds me of this story. In a nutshell, it goes like this: “X is causing us lots of problems. Maybe if we use a system that prevents us from doing X, we’ll stop having these problems.”
This argument shows up in a lot of forms. The Java language was sold to the programming establishment partly on these grounds: “programmers keep writing terrible code in C++. If we switch to a language that strictly limits what they can do, they’ll stop writing such terrible code.”
I’ve heard it used to justify a switch to a functional programming language: “we know that mutable state is the root of many evils. If we prevent mutable state from existing, we won’t have those problems.”
Lately I’ve been messing around in Go, and it contains more than a hint of this kind of thinking: “Programmers are bad at handling failure modes. Maybe if we force them to surround every function call with its own if/then/else, they’ll do a better job of thinking about failure.”
It’s not strictly an organizational phenomenon. People use this strategy at a personal level all the time. “As soon as I get a new bike, then I’ll get into shape.”
And it often works, too. A shiny new tool and a clean break from the old ways can be a great motivation to get out of a rut.
At least, it often works for a while. For the first couple of months, that new bike gets lots of exercise. But then the weather gets cold, and some spokes get bent, and they can never seem to remember to take the bike to the shop. And the weeks pass, and then months.
Not coincidentally, when we’re in this honeymoon period we want to talk all about it. The person with the new bike constantly posts their latest stats to Facebook. The team with a new programming language blogs all about how it has revolutionized their work. Our industry is full of experience reports from three or six months in.
But then the Facebook posts and blog posts start to peter out. For some, this means a new, better normal has settled in. But in other cases, it can mean that things aren’t going so well anymore.
The lure of microservices
Most recently I’ve started to see “microservices” treated as if they are one of these “liberating constraints”. By splitting our apps into dozens of tiny programs, we’ll finally enforce modularity. We’ll stop building big balls of mud.
Of course, those services still depend on each other. We’ve just given them (hopefully) well-defined interfaces. And we’ve pushed their interconnections and dependencies out from a realm where our tools could visualize them as tangled webs, into a realm where our tools can give us no insight whatsoever. We’ve moved our diagnostics from tests into distributed log files. And we’ve almost certainly introduced new implied temporal dependencies that didn’t exist before.
There are definitely teams that are succeeding with microservices. There are also teams who are succeeding with monoliths. Facebook is still one single giant codebase. Every tiny tweak increments the entire app.
Martin Fowler has been observing and documenting the rise of microservices. When we last had him on Ruby Rogues I took the opportunity to ask him about what he had seen so far. He had this to say:
I’m still a little bit unsure about the whole microservices thing… exactly when you should do it is an interesting question, because there are a lot of benefits from having separate services. You can independently deploy the services. You can independently scale the services. You get an enforced modularity which most programming languages’ environments just don’t give you. You can really solidly say what your interfaces are. And you can make damn sure that you’re never passing mutable data across module boundaries, things like that.
But on the other hand, as soon as you move to a distributed design, distribution is a huge complexity booster. And in order to get distribution to perform effectively, you’re probably going to have to go to an asynchronous call mechanism. And asynchrony is a huge complexity booster. And so, you take on quite a price. And the tradeoff between those two things is really quite significant.
And this also plays into the sacrificial architecture idea, because your first, your early attempt, you probably don’t want to go down the microservices route because you don’t really know what your module structure’s going to look like. You’re still trying to figure out what on earth’s the system you’re trying to build from a user experience and from a functional sense. So, you want to be able to rapidly change things within the structure. And only when things begin to solidify is it a good idea to peel out things into separate services.
But this area is still very new. We’re still really getting only the first indications of what is a good and bad practice. So, the best I can do is listen to the people and try to pick up what I can and distill as much signal as I can from all the stuff I’m hearing.
What’s my motivation?
I don’t think microservice architecture is a bad idea. I’ve started using it a little bit myself, writing a separate service to do some periodic processing instead of adding to an existing application. There was something freeing about not worrying about how to control duplication between the two programs. On the other hand, it took roughly 30 seconds of development before I’d introduced a bug by writing a regular expression slightly differently in one program than it was written in the other.
This article is really just about motivations: why we choose new tools, or new architectures. We know that certain bad habits lead to unmaintainable codebases. And yet we keep falling into those habits. And it’s very seductive to think that when we switch over to that new framework or that new language, we’ll kick all those bad old habits at once and ring in a new era of clean coding.
I think it’s important to reflect on why we are really considering a change to our development stack. Is there a chance we’re just looking for a box and a piece of string and a high shelf to keep us from eating those tasty, tasty cookies?
Perhaps what we really need is some willpower.