A growing development team can be super exciting. Imagine all the new stuff we'll get done! There'll be so many more people to learn from. And it probably means that the business is moving in the right direction.
But at some point, you'll find yourself getting less done with more people. Maybe you're starting to hear about how the work feels like it's dragging. The words "unmaintainable" and "stuffed full of bugs" start to get thrown around.
Just as communication gets exponentially harder as a team grows, code can become exponentially harder to work with as the team building it grows. How can you tell if it's happening?
Onboarding is harder. The goal was having a new dev shipping on the day they join. But now it takes up to a week to get their environment set up, and to learn enough about the code to make a simple text change.
You can't easily make a change without breaking unrelated code. Even if the team has been good about reducing coupling, over a few years, if code can be coupled, code will be accidentally coupled.
In the best case, this costs dev time. In the worst, it causes errors or site downtime.
Shipping takes longer. When everyone's working in the same codebase, you'll have a decision to make: Do you want to batch changes and ship them all at once? Or would you rather have everyone wait in line to ship?
If you decide to batch changes, you'll have a lot of integration pain. If you decide to have a ship queue, that queue will grow as you add more devs and ship more often. Instead of working on the next thing, devs spend half their day at half-attention, waiting for their code to go out.
Tests take longer to run. You want all the tests to pass before you deploy, right? So not only does this make you lose more time when you're shipping, it also delays everyone in line behind you. Hopefully your tests pass consistently!
Ownership becomes unclear. When something breaks inside the code, who's responsible for investigating and fixing it? When ownership is muddled, entire features become "someone else's problem," and don't get the care they deserve.
What do we want?
What would a better world look like? It'd be awesome if we had:
- Apps based around simple ideas that are easy to understand.
- Isolated sections of code, that can't affect each other.
- Loose dependencies, so small pieces of the app can ship independently.
- Fast tests.
- Clear ownership.
Small apps or services, coordinating to get work done, hit every one of those factors. Because, in general:
- A smaller app is easier to understand than a large one. There's less code to worry about.
- If you isolate code inside a service, another app can't mess with it. You can only make changes through the interface.
- If you know what a service's clients are expecting from it, you should be able to change and ship it independently.
- If you're shipping a smaller piece of a large app, you'd only have to run some of the larger app's tests to feel comfortable shipping it.
- It's easy to assign ownership of all of a small app: "Hey, Katie, you're responsible for the Account service."
All of these benefits have contributed to Microservices becoming a trend.
The traditional fix to exponentially growing communication is to break large teams into smaller autonomous teams. This works for software, too. It can turn exponential growth into more linear growth. But it's good to understand why this works -- what problems microservices are meant to solve. Because, like every decision in software development, it involves tradeoffs.
If you're not having any of those problems I described earlier, it's probably not a great idea to transition to services. Because there are some pretty big problems with a service-based architecture:
The code may be simpler, but the relationships between coordinating services become more complicated. This is something your language probably won't be able to help with. You'll need to work to make that coordination visible, so you can see those connections and make sure they make sense.
Services can add a lot of busywork when you start building a new app. If you have an app, a client, a contract, and a service, you'll sometimes have to tweak and ship four repositories to get any work done.
Tests are harder to write, and can be more brittle. A lot of your tests will depend on the network. You'll have to accept the brittleness of relying on code running somewhere else, or mock the connections out. If you're mocking, your tests might pass when they should fail, which is a big problem.
You and the team have to define solid patterns for communicating. You'll decide how to communicate errors and metadata, how and when to retry failed requests, deal with caching, serialize and deserialize data, and lots of other things. This is stuff that a big monolithic app will give you for free.
You've now built a distributed system. Distributed systems, especially when things fail, act unpredictably.
If you're using HTTP to communicate between services, you've probably added latency. Your app might be slower, or you might add caching to try to speed things up. If you do that, you have to worry more about one of the two hardest problems in computer science -- cache invalidation. Not only that, but cache invalidation across multiple apps.
And there's even more! These sound like huge problems, and they are -- but then, so are the problems related to a growing team on a growing app.
The key is recognizing when the problems of team and code growth are on the path to outweighing the problems of a service-based architecture.
Most of the companies that have moved to a microservice architecture seem very happy about it. We're still in the process of making the transition, but so far we've seen huge wins from it.
If helping us make that transition sounds interesting to you, let me know -- we're always looking for great new people to join our team.