The issue is coordinating changes (and, god help you, library releases) across repos is often an utter nightmare with multiple PR/merge builds
The issue is coordinating changes (and, god help you, library releases) across repos is often an utter nightmare with multiple PR/merge builds
I'm coming up on a year out of $BIG_TECH_JOB (where the idea of a monorepo was horrifying) and transferred to $WAY_SMALLER_NON_TECH_COMPANY_WHO_USES_TECH_JOB (where the enthusiastically use a monorepo), and have been really confused by repeated claims like this. I really feel like I'm missing something here, because lots of obviously-smart-and-experienced folks repeat it. I'd really appreciate it if you could check my understanding and see what I'm missing.
(To be clear, here I'm assuming that a monorepo is a single repository which contains conceptually-distinct-but-related projects - things which _could_ justifiably be their own repos, but which are kept in one repo for reasons of maintenance and managements - and wherein the build system is such that every sub-project within the repo uses the same package dependency tree, i.e. if ProjectA and ProjectB in the monorepo depend on LibraryZ, then the versions of Z that A and B depend on must be identical for any given commit/build. If I've misunderstood that - if that's just straight-up not what a monorepo is, or if it _is_ but with some extra nuance or sauce - then I guess we can short-circuit the response pretty quickly :P )
Here's how a release of a breaking change of a library would work in a polyrepo world:
* I publish v2.0.0 of my library
* Consumers of that library are notified that a new version exists (via automated email notification, Dependabot, whatever) \ * Anyone who _wants_ to update can do so (independently and at their own rate) - anyone who's comfortable staying on the old version can do so
** If we really want to make things easy for consumers, we can make automated PRs against their repos (this would be the "extremely good build/integration tooling to deal with many repos" you refer to, I suspect? Something like Spotify's FleetShift[0]) to make the change - though in practice this is probably way more trouble than its worth, I've only seen it done for serious security vulnerabilities where a) everyone in the whole damn company has to b) upgrade RIGHT THE FUCK NOW.
* Time goes by
* v1.x gets deprecated
* Anyone still using v1.x gets notified that they are using a deprecated version and strongly encouraged to update
* A little more time goes by (not much!)
* Consequences Occur for people still using the old version - this could be a visit from your friendly InfoSec enforcer, or automatically failing builds, or the removal of v1.x from the package repository (which will indirectly cause failing builds), or...
Conversely, with a monorepo, the situation seems to be:
* I publish v2.0.0 of my library
* Every single team whose code is in the monorepo must coordinate to make the changes to consume version 2. This change, by definition, proceeds at the pace of the slowest team - if one of them is underwater on oncall, or has their only competent engineer on PTO, or has some quirk of implementation (or dependency on old feature) which means they can't update for two months, then _the whole dang monorepo_ is staying on version 1 for two months; no ifs, ands, or buts
** But, yes, if you want to a wide-ranging refactoring to change all the in-monorepo consumers to use the new method, then yeah, modern IDEs are going to have _some_ functionality built-in for that within a single repo. But - by virtue of being a breaking change, it's pretty likely that the change-at-call-site is going to be more complex than simply changing the type or name of the method being called. Maybe the returned object needs different methods called on it. Maybe the method requires an extra parameter (which refactoring IDEs can add to the actual callsite, but cannot implement for you _fetching and providing_ that parameter). At this point, either humans are going to have to comb through the changes to finalize them (at which point, you lose the claimed advantages of being in a monorepo where changes can be done by tooling), or you're going to have to implement some sort of code-parsing system to make correct changes throughout (which, again, is approximately equivalent to the automation work required in the polyrepo case)
So: while, yes, it _would_ be a hassle to "coordinate changes across repos" (though _significantly_ less to coordinate across a single repo-to-repo boundary, especially if both are owned by the same team, than it would to coordinate a whole monorepo's worth of interactions), the joy of a polyrepo situation is that _you don't have to_. Consumers can update when they want to, at their own pace - no coordination necessary! So - yes, there will be a greater _volume_ of PRs required in a polyrepo situation, but a) each of them will be way simpler and the total volume of work will be , b) they are independent (so teams who don't want to update do not hold back those who do).
But - people keep saying "it's easier to make wide-ranging changes in a monorepo", so I _must_ be missing something. What is it?
[0] https://engineering.atspotify.com/2023/05/fleet-management-a...
There are already incentives in place to ensure that a library provider is doing right by their consumers (building features that they want, making upgrades not too arduous), because if they make a hard-to-use library then people won't use it which should show up in however their team's success is judged. Seems to me that going the extra mile from "you have to provide a good desirable library" to "and you must also be familiar enough with your consumers' services to do the integration/upgrade work _for_ them" gives no new upside but all downside - service teams _already_ know their domain area, why should someone else have to?
(I recognize that you're just reporting what you've heard, I'm not arguing _with you_, I'm trying to reason out the arguments in my head to get them straight)
Really appreciate the insight, thank you!
I’m curious about this aspect that you mentioned:
> forcing a library provider to maintain familiarity with all their consumers' codebases and business areas
In my experience, this hasn’t been an issue—the library authors are inherently intimately familiar with how to accomplish transparent upgrades of any of their library’s API changes in a consuming codebase. Or if there are breaking API changes that make it impossible/infeasible to maintain the current consumer behavior, that’s a very useful signal that it’s time for the library producer to go back to the drawing board because the proposed library update has issues—before it’s “too late” to rethink because the library update was formally released :) Or at least, helps trigger a conversation between the producer and consumers to see if the breakage can be accommodated without too much heartburn, or if it’s a non-starter and a pun upstream solution that works for both parties needs to be discussed/implemented.
Overall, it really reduces the “library producer in their ivory tower releases changes without fully considering the downstream impacts, causing a crapload of burden/schedule slip for XX teams” issue. And when you multiply that by N library producers in a company of a given scale, it’s easy for app/service dev teams to have a lot of their time non-productively taken up just trying to keep up with the constant treadmill of poorly handled upstream changes. So removing the externality effect by having the producers have some “skin in the game” can really help improve the experience for the consuming teams.
hth
> I’m curious about this aspect that you mentioned:
>> forcing a library provider to maintain familiarity with all their consumers' codebases and business areas
> In my experience, this hasn’t been an issue—the library authors are inherently intimately familiar with how to [make changes] in a consuming codebase
(See my comments below where I'm not sure what a "transparent upgrade" means)
Wow - that's genuinely surprising (and impressive!) to me. This implies that, for all N consumers of the library, the library team are already familiar (without having to look it up - otherwise, that's added ramp-up burden) with the consumers' preferred code style, testing expectations, commit and branching structure, personal development environment setup, running integration tests against personal environments, how to announce PRs (if at all), and so on - all the "other stuff" that goes "around" making the actual code change. In an ideal world, a lot of those things _should_ be trivially clear/provided (code-style provided by auto-linters, testing carried out automatically during PR, etc.) - but practically speaking they very rarely are. This seems like a _lot_ of extra knowledge which is (in my opinion - though clearly not in a monorepo-mindset opinion) outside of their area of ownership and expertise that they have to carry around on a day-to-day basis, on the off-chance that they make a breaking change to their library (which, as I think we both agree, should hopefully be rare).
Even in the best-reasonable case, where all of those things are well-documented (and they _should_ be, because an outside contributor is indistinguishable from a New Hire), that's still a fair bit of reading, ramp-up, and workspace-setup that the library team needs to do - vs. simply telling the consuming team "here's the general shape of change that you need to make, apply this as appropriate to your own situation - ask us if anything's unclear"
OK, on to the direct reply:
---
So, to be clear about terminology, I think we're only really concerned with breaking changes, here - i.e. those for which, if the consumer moves from consuming v-<previous> to v-<new>, they will also need to change something about how their code calls the library. The most obvious ways I can think of for this to be necessary are:
* The type signature changes in a non-backwards compatible way - i.e. adding required parameters (including making previously-optional parameters required), changing the type of a parameter, or changing the type of the return value to something incompatible with the previous type ("incompatible" is hand-waving because different type systems think about this differently, but I think my meaning is clear in general) * The name of the method itself is changed, or the entire call pattern is changed (e.g. instead of `foo.execute(command)`, you now have to call `foo.prepareCommand(command); foo.execute()`)
For any library change which _isn't_ a breaking change (i.e. one for which the consumers could keep their call-site code exactly the same, and expect the same behaviour) I believe this whole discussion is moot - both perspectives would agree that nothing would need to be done in the consuming code. My perspective would say that the consuming team has nothing to do, and the monorepo perspective would say that the library team has nothing to do - but, either way, `0 === 0`, even JavaScript agrees on that :)
So, then, I might need a bit of elaboration on what a "transparent upgrade" is, since my initial naïve interpretation (a response to a non-breaking change - one in which the call-site code can remain the same) cannot be the thing at issue, since - well, yeah, of course a library team knows how to make a no-op change to their consumers ;) might "transparent upgrade" instead mean "a change where all the data required to make the library call is available at the call-site, but it needs to be reshaped, retyped, or otherwise reorganized before being passed to the library function in v-<new>"? If so, then...yeah, I can see it being _nice_ if the library owners were to publish some deterministic code-diff tool which would transform all their consumers' call-sites to use the newly-reshuffled data, but honestly I'd expect them instead to do that "reshuffling" _internally_ to their function and not even publish the new "breaking but with reorganized data" version in the first place. Maybe there's a good case for this "data-reshuffling" that I'm missing, though.
Maybe a pseudocode example of a "transparent upgrade" would help me understand what it is?
---
> if there are breaking API changes that make it impossible/infeasible to maintain the current consumer behavior, that’s a very useful signal that it’s time for the library producer to go back to the drawing board because the proposed library update has issues[...]Or at least, helps trigger a conversation between the producer and consumers to see if the breakage can be accommodated without too much heartburn
Yeah, I think we're generally on the same page, here; just assigning differing weights, priorities, and likelihoods to breaking changes. Wherever possible, for sure, aim for non-breaking changes; and where breaking changes are necessary, make sure you have had a full discussion with consumers to see whether the churn you're going to create is justified (and maybe consider providing a long period of support for the previous version so that consumers have a long period to upgrade and to amortize that upgrade-churn) - but, if a breaking change _is_ truly justified (is worth more than the upgrade-burden it will cost), don't shy away from it!
> Overall, it really reduces the “library producer in their ivory tower releases changes without fully considering the downstream impacts, causing a crapload of burden/schedule slip for XX teams” issue.
Maybe I've just been very lucky to have barely run into that problem! :) In ~10 years in my previous role, I'm struggling to think of a handful of times where a major version upgrade was any more than a trivial process - and the few times that it _was_ a meatier change, I was perfectly content with being given a guide or walkthrough on how to migrate. But, yeah - if cavalier library teams making profligate and unjustified breaking changes _is_ a problem being faced, then I can see that that culture of "you break it, you fix it" would rein them in.
---
FWIW, this has _already_ really helped me to understand where a coworker was coming from in an (unrelated) proposal which, to me and all other colleagues, initially looked nonsensical. After chewing on it a bit, I realized he's coming at this from the same perspective - "people who make changes to things that other people depend on, should bear the pain of updating those other people's things to work with the new dependency-thing". Gotta say I am still extremely-unconvinced (I'm still on the side of "ceteris paribus, an owner making a change is more efficient than mandating that someone else make the change; so I'd prefer to find other ways of forcing library owners to confront the externalities of disruptive breaking changes that don't introduce that inefficiency"), but it does very much help to understand where he's coming from - thanks!