Hacker News new | ask | show | jobs
by nurettin 1613 days ago
> A benefit of microservices is that each team can be responsible for releasing their services independently and without coordination with other teams.

Sounds almost sarcastic. How do you deliver API changes without alerting other teams?

12 comments

This is how things were done at Amazon quite successfully. The golden rule is to never break API backwards compatibility. If you must, create a new version of the API and leave the old version functional. If you need to shut down old functionality, it becomes a campaign you have to drive to move your dependents off of it. One little thing that people often overlooked but was very important was to have API operations for describing your enums rather than just putting them in the docs. This allowed for easier adoption of them by API consumers and forced them to consider what to do in the case of an unrecognized value being encountered.
>The golden rule is to never break API backwards compatibility. If you must, create a new version of the API and leave the old version functional

It also helps with zero-downtime deployments:

1) spawn a new instance of the service with the new API, side by side with the old one

2) now incoming traffic (which still expects the old API) is routed to the new instance with the new API, and it's OK, because it's backward-compatible

3) shut down the old instance

4) eventually some time later all clients are switched to the new API, we can delete the old code

How can accumulation of versions be prevented? Now the same team has to maintain two products, and the underlying mechanism is still limited by the older version.

Anecdotally, a robust backward-compatibility has been seen as a hinderance to e.g. Java's progress (so much that a newer language, Kotlin, was created to break free from that burden).

There isn't really a solution to version bloat aside from good processes and general diligence. There's no easy way to handle that sort of thing, unfortunately.

However, I do think it can be easier to deal with for internal services then for something like Java. When the number of users is in the dozens rather then the millions, it's a lot easier to make sure everyone gets moved over to the new version.

That's what makes the whole idea of microservices seems weird to me. If a functionality has merit on its own (e.g. an authentication service), then it will naturally fall outside of the main application. If a service is tightly coupled to other parts of the app, then microservices seems like intentionally hindering yourself: the coupling remains (as evident by the need for backward compatibility), but now we it's harder to keep everything aligned due to the extra separation (e.g. different code bases, multiple databases, no static validation of remote interfaces etc.).

The point of organizations and products is to work as a tightly coordinated machine. The decoupling that microservices create seems opposite to that goal.

Happy to hear different perspectives.

I agree that tightly coupled modules should live inside the same deployment, but:

>it's harder to keep everything aligned due to the extra separation (e.g. different code bases, multiple databases, no static validation of remote interfaces etc.)

It's solvable with appropriate tooling. I.e. you can store API definitions in a separate repository and make the services or CI/CD check API usage is valid at build time.

>tightly coordinated machine

What do you mean by that? For example, we have 10 teams all developing different features in parallel, with a tight release schedule. If there was tight coordination for every change, we'd degrade to a waterfall on the scale of the whole organization. Major API changes are discussed in advance during P/I planning; for minor changes, it's a matter of simply notifying other teams "hey, add this to your backlog, please" (we enforce backward compatibility for zero downtime anyway, so it's not urgent)

One way is to rewrite version N endpoints to use version N+1 endpoints. You just need to ensure clients can handle null/empty data so that when some requested data is depreciated, you don't break old apps. The increased latency from N conversion calls also encourage the oldest clients to migrate without breaking backwards comparability.
Well we usually coordinate between the teams. I.e. we don't force other teams to make changes as soon as possible (they have their own plans) but we agree to add relevant changes to their backlog, so that it was fixable in a 1-2 month window.
You need to talk to the other teams. Usually the change isn’t so drastic, I often made the change myself in the other teams service and sent them a review.
And still done! Fully agree.
> Sounds almost sarcastic. How do you deliver API changes without alerting other teams?

Sounds almost sarcastic. To deliver API changes without alerting other teams you, of course, simply deploy the changes without sending a message to the other teams.

The non-sarcastic answer is that sometimes you want to make changes that will not affect an APIs users in any significant way. Of course you would still document these changes in a change log that the consumers of the API may or may not check. Or you may want to hype/market these changes for clout reasons.

Maybe it's an API that services multiple sets of users with different partially-overlapping requirements and they don't all need to know about the new change.

Maybe it's a soft launch for a surprise feature that's going to be announced later.

Maybe the other team is on vacation and you just want to get changes out the door before some holiday.

Do you not version APIs you design?

When engineering an API meant for consumption by disparate services it’s imperative to provide back words compatibility.

This is pretty basic stuff anyone designing a serious API should be taking into account.

Sure we do, it's been /v1.0/ for the last 2 years!
All API that I am running had been v1 the whole time.
Yes, and I have to consult with other teams when versioning, I don't just go "welp here's the next version I created and deployed without asking anyone because that's what the IBM microservices manifesto (2006) suggested and some random guy on HN insinuated I'm causing undue friction if I work with you guys"
sure, but all of that is a cost which you don't have to pay with a monolith. Versioning APIs and having to constantly think about backwards compatibility with independently moving services is not trivial.

Sometimes the cost is worth it. Most of the time it's not

Oh, you absolutely have to pay that cost with a monolith, it's just less clear and obvious because you can change all of the consumers when you make a change to an interface.
Also, this (independent deployability) is simply not a feature of microservices. It is a feature of any well architected code base.

I've always worked on monoliths, and I've almost never needed to coordinate a release with anyone. I just merge my branch and deploy. Github and shopify talk about merging and deploying monoliths hundreds of times per day without coordination.

The case where you would need to coordinate a release in a monolith is exactly the same case where you would need to coordinate a release in microservice app. That's the case where your change depends on someone else releasing their change first. It doesn't matter if their change is in a different service, or just in a different module or set of modules in the same application.

Now, most application are not well architected - micorservices or monoliths. In the case of a poorly architected app deploying a monolith is much easier anyway. Just merge all that spaghetti and push one button, vs trying to coordinate the releases of 15 tangled microservices in the proper order.

Don't ever change APIs. It's pretty simple. I don't know why the monolith people believe this is such a gotcha.

If you really need to change the API, give the new API another name. You may choose to think of this as "versioned APIs", if you want, but "versioned" and "renamed" are the same thing.

Proper deprecation procedures. you can document how you uniformly deprecate and remove APIs. This is a strength of using something like OpenAPI for documentation, or GraphQL, for instance. It is then the responsibility of a consumer to deal with these deprecation(s). On the most basic level you could also do versioning, though its not my recommendation

Document and set expectations accordingly. I've done this move before breaking apart a monolith into separate micro services and this is key. Spending more time on good documentation is generally a good idea regardless.

I'm assuming we're not talking about public facing APIs. That's a situation where versioning might make a lot more sense.

Generally speaking if you're adding another field to a JSON or something, that doesn't really break the parser [1] or affect downstream. While you should still probably let the downstream teams know, it's not necessarily going to break anyone's code.

[1] I'm aware that that's not always true (e.g. adding a field that's ridiculously large choking up the parser).

or you have a customer that has strict validation on and adding a field breaks their ability to deserialize.
Something like Microsoft does with the their interfaces

They have multiple versions of calls. The older one function as before and never change. Want different behavior - here is your_interface_v1(), your_interface_v2(), etc.

You still alert team about new functionality but they're free to consume it at their own pace. This of course involves a boatload of design and planning.

I am in general against microservices and consider those as the last resort when nothing else works. To me a microservice is mostly my monolith interacting with another monolith.

When monolith becomes big enough that it needs 2 teams I usually handle it by each team releasing their part as a library that still gets linked into the same monolith. That is my version of "microservice" when the only reason for it to exist is to have two or more "independent" teams.

I'd guess that 90-95% of tickets do not alter a existing API in a non-backwards compatible way
Not all changes result in a change to the way your service is called, and even those changes can (with some effort and care) be made backwards-compatible. Performance-level changes are one obvious one - for example, I wouldn't expect to have to keep my caller in the loop if I decrease my API's latency by 50ms, even though it might be a good idea.

But other behavior changes are also not necessarily something that requires a team to be alerted. A good design provides an abstraction where the caller shouldn't have to care about the underlying implementation or details of how a request is fulfilled.

Never break the API. If you need a new API contract use API versioning so that consumers can upgrade when it's convenient. Additionally, use contract testing.
That's why finding the right boundaries between services (yes, services, microservices is a harmful buzzword) is important, so that you minimise having to communicate and coordinate with other teams.
100%. Boundaries are extremely important, and if you're a service onto which 7 other teams rely on, there's an issue with your teams and the way you've setup your services. Bounded contexts!