Lesson 49. Does This Framework Earn Its Place
Mission link: Owning a Java service in production ends, sooner or later, with someone asking whether a framework belongs in it, and this stage's final test is answering that from the service's own constraints and being able to defend the answer to whoever inherits it.
Primary source: Choose Boring Technology, Dan McKinley
Prerequisites: Lesson 48, Lesson 33
Warm-up
Lesson 33 taught you to run mvn dependency:tree -Dverbose and read exactly what a single declared dependency drags in behind it, at versions nobody on your team chose. A colleague proposes adding one line to the pom.xml, a framework's web starter, before a single controller has been written against it. What has that one line already committed the project to, and how would you find out exactly what, without reading a word of the framework's own documentation?
Check
The full transitive graph the starter pulls in: an embedded server, a JSON library, a logging binding, and whatever each of those needs in turn, all at versions the starter's authors chose rather than the team. mvn dependency:tree -Dverbose, the same command lesson 33 used to read a version conflict, answers it directly and in minutes, before anyone has written a class against the framework or formed an opinion about whether it is any good. That list is the cheapest fact in the whole judgment this lesson teaches, and it is available immediately.
Know this
Why the question has no general answer
Whether a framework belongs in your service is not a question with a general answer, and anyone who claims to settle it in general, for every team, on every service, is answering a different question than the one you actually have. A ranking of frameworks looks like engineering advice and functions like a preference dressed up as one, because the thing that actually decides the case is never in the ranking: your team's size and its familiarity with the idioms involved, the service's expected lifetime and its traffic shape, the cost your organisation can absorb if the choice turns out wrong, and the specific problem you have today rather than the interesting problem you might have someday. Searching for an authoritative source that settles "does a framework earn its place" in general turns up nothing that survives scrutiny, and that absence is not a gap in the reading, it is the correct shape of the answer. What does exist, and what this lesson gives you, is a rubric and the discipline to apply it honestly to your own situation rather than to the situation the framework's documentation imagines you are in. That discipline is worth more than a verdict, because a verdict borrowed from someone else's constraints is exactly as wrong for you as their constraints are different from yours, and you rarely find out how different until the cost lands.
The cost side, which is the half people underweight
A dependency is not free at the moment you add it. It is free at the moment you add it and expensive later, in places the pull request that added it never touches, which is exactly why the cost is easy to underweight: the benefit is visible on day one, in a demo, and the cost is distributed across years and across people who were not in the room when the decision was made. Six places the bill actually arrives.
The first is the transitive graph itself, which lesson 33 already taught you to read. A framework's starter is rarely one jar, it is a subtree, and every jar in that subtree is now a jar your service ships, patches, and answers for when a vulnerability scanner flags it, whether or not a single line of your own code calls it directly. The size of that subtree is a fact you can measure in minutes with the tool lesson 33 gave you, before you have formed any other opinion.
The second is the upgrade treadmill. A framework has its own release cadence, its own deprecation cycle, and its own opinion about how long an old major version stays supported, none of which your service's own schedule was consulted about. Falling behind on it does not stop the clock, it just changes what falling behind costs: security patches stop arriving for the version you are pinned to, the gap between your version and current grows every month you do not move, and the eventual upgrade stops being a routine bump and becomes a project with its own name and its own risk of breaking things the way lesson 43 taught you a binary-incompatible change can.
The third is the failure modes you now have to understand but did not write. A boring, well-worn technology fails in ways many other teams have already hit and argued about in public, so its failure modes are largely known unknowns: you may not know exactly when a connection pool will exhaust itself, but you know that it can, and you can go looking. A framework still finding its footing hands you failure modes nobody has hit yet, which stay unknown until the day your service is the one that finds them in production, and it is your problem alone to diagnose.
The fourth is the way a framework's idioms propagate through code that never asked for them. A framework that only works if your domain classes extend its base class, or carry its annotations, or follow its lifecycle, is not charging you once at the boundary, it is charging every class that has to know about it. Stage 2 already taught you what extends costs beyond the method you inherit, and a framework that insists on being your superclass is asking your domain model to pay that cost everywhere it touches the framework, not at one clean seam.
The fifth is hiring and onboarding. Every framework a service depends on is a thing a new engineer has to learn before they can be trusted with the parts of the code that touch it, and that is real calendar time spent before they produce anything, repeated for every person who ever joins the team. A framework nobody on the team has used before is a bet that the learning curve is worth climbing, paid by whoever climbs it.
The sixth is the exit cost, and it is the one nobody estimates, because estimating it means imagining a future in which today's choice turned out wrong, an uncomfortable thing to do in the same meeting where you are arguing the choice is right. Make it concrete with one question: what would removing this actually touch? Count how many classes import the framework's types directly rather than your own abstractions, and check whether your business logic can be tested at all without the framework's container running underneath it. If the honest answer is "most of the codebase, and no", the exit cost was already paid the day the framework was allowed to leak everywhere, whether or not anyone ever intends to leave.
The benefit side, honestly
A lesson that only lists costs teaches the wrong reflex, because the reader will meet plenty of problems they should not try to solve themselves, and refusing every framework on principle is just a different, equally lazy answer to a question that deserves an actual answer each time. What you are usually buying, when a framework genuinely earns its keep, is not a feature, it is somebody else's already-solved problem: code exercised by far more users, in far more configurations, than your service will ever produce, maintained by people whose job is keeping it correct so that yours does not have to be. Undifferentiated, necessary work, the parts of a service that have to exist but are not the reason it exists, is where this trade is at its best: routing HTTP requests against a specification with edge cases nobody enjoys reading, pooling connections correctly under contention, handling character encoding and streaming correctly for payloads you did not choose the shape of. Building any of that yourself does not make your service better at what it is actually for, it just means you now own bugs that a larger, better-resourced group of people have already spent years finding and fixing in the library you declined to use. Say it plainly: when the problem is genuinely not yours to solve, adopting the boring, well-tested answer to it is the correct engineering decision, not a compromise, and the rubric below exists to help you tell that case apart from the one that only looks like it.
The rubric
A short, ordered set of questions, meant to be asked in this order because an early "no" usually closes the case before the later questions matter.
- What problem does this solve that the service actually has right now, stated without a hypothetical? If the honest answer names a requirement nobody has yet, a scale you have not reached, or a backend you might switch to one day, this is speculative generality, the same failure Fowler's "Yagni" names in code: paying for flexibility before you know what shape the flexibility needs to have, when your current guess is unlikely to match what you actually learn later.
- What is the smallest thing that would work, and what specifically does it fail to do? This is the question that most often settles it. Write down the smallest hand-rolled version of the same capability, honestly, and name the exact gap between it and the framework: not "it feels less robust", but the specific case it mishandles, the specific effort it would take to close that gap, and whether that effort is smaller or larger than adopting the framework and paying its costs.
- How much of your organisation's limited appetite for novelty does this spend, and what else was that appetite earmarked for? Every team can absorb only so much unfamiliar technology at once before its ability to operate anything confidently starts to degrade. Spending that budget on the thing that makes your product different from its competitors is usually a good trade; spending it on plumbing that every competitor already buys off the shelf usually is not.
- What does the transitive graph actually contain, once you have read it rather than guessed at it? Lesson 33 gave you the tool. A framework whose starter pulls in four jars is a different proposition from one that pulls in forty, and you should know the real number before the next question rather than after.
- What would it cost to remove this today, before it has had time to spread? The answer now, while the surface area touching the framework is still small, is the cheapest estimate of the exit cost you will ever get. It only grows from here.
- What would have to be true for you to change your mind? If nothing would, you have not made an engineering decision, you have made a commitment, and the difference matters for the next section.
The special case of a framework you already have
Most of the time the question you actually face is not "should we adopt this", it is "is this still earning its place", and that is a different, harder judgment because two things are true of it that are not true of a fresh adoption. Sunk cost is irrelevant: the years already spent integrating a framework, and the pride of whoever chose it, are real, but none of that makes the framework cheaper to keep from this point forward, because none of it can be spent again. Migration cost is very relevant and is the opposite of sunk: it is the cost still in front of you of moving away from something wired into the codebase, and it has to be priced against the cost of continuing to pay for the framework's problems, not against the cost of never having adopted it. Ask the removal question from the rubric again, now with real data instead of a guess: how many places touch the framework directly, whether the business logic underneath it can be exercised without it, and how much of the team's regular time already goes into working around it rather than building what the service is for. A framework that continues to save more than it costs, quietly, without regular incident, is earning its place even after years, and "we have always used it" is not evidence either way on its own. A framework the team spends more time working around than working with, where the original reason it was chosen no longer holds, is not earning its place regardless of how expensive leaving would be, and the honest next step is pricing that departure rather than avoiding the question because the price looks large.
Explaining the call to someone else
The mission's whole point is being trusted to make this call and to explain it, and the explanation has a shape worth learning on purpose rather than improvising each time. State the constraint that actually decided it: the team's size, the deadline, the traffic the service really carries, whichever fact from the rubric was the one that closed the case. State the cost you accepted, in the terms this lesson gave you: which line of the cost side you are paying, and roughly how much of it. State the fact that would change your mind, from question six of the rubric, concretely enough that someone could actually check it later. A decision recorded that way, with its reasoning attached to a constraint, can be revisited the day the constraint changes, because the person doing the revisiting has been told exactly what to check. A decision recorded only as a preference, "I like this framework" or "everyone uses it", cannot be revisited at all, because there is nothing underneath it to test against new facts, only a mood to overrule, and overruling a mood is what starts arguments rather than settling them.
Stage 7, closed
Seven stages ago you could not yet be trusted with any of this. Stage 1 gave you the contracts underneath every collection you have used since; stage 2 gave you the composition-against-inheritance judgment this lesson has now leaned on more than once; stage 3 gave you the library's own idioms; stage 4 gave you a memory model and a way to reason about correctness under real contention; stage 5 gave you a build and the dependency graph this lesson keeps sending you back to; stage 6 gave you the runtime itself and the discipline to measure before believing a performance claim; stage 7 has been about turning all of that into calls you can defend rather than facts you can recite, whether a change is safe to release, whether a signature is the right one, whether a source settles an argument or only informs one, and now, whether a framework earns the cost it charges. None of those seven stages made you know more Java than the person sitting next to you. What they gave you is the ability to price the Java you already knew, to look at a keyword, a library call, or a whole framework and say specifically what it costs, what it buys, and under which constraints the answer changes. That turns out to be the whole of judgment: not a bigger vocabulary, but an honest price on every word already in it. Stage 8 follows, naming the mechanism, modules, runtime images, and reflection, that every framework this stage judged is actually built from, but it teaches no further judgment. What comes after all of it is a service you actually own, a call you actually have to make on it, and someone standing in front of you who needs the reasoning, not just the answer.
Practice
- ▢ A three-person team is building an internal admin tool for under fifty users, on a tight deadline, and is deciding between a full framework with dependency injection, an object-relational mapper and a built-in scheduler, or a small HTTP server with a plain connection pool and hand-written SQL. Using only that constraint, predict which is the better call, and say what would have to change about the constraint to flip the answer.
Check
The small server is the better call here. At fifty users and a tight deadline, the framework's benefit, other people's solved problems at scale and under load, is not a problem this service has, while every item on the cost side, the transitive graph, the learning curve, the upgrade treadmill, is paid in full regardless of scale. The answer flips if the constraint changes: the team grows and needs new engineers to onboard against a codebase every year, the tool's audience grows into thousands of users with real load, or the tool's scope grows past admin duties into something the business depends on, at which point the framework's solved problems start to be problems this service actually has.
- ▢ A service has used a persistence framework for three years. Two of the team's five engineers now spend regular time each sprint debugging extra queries the framework's lazy loading triggers in production. Predict whether the three years already invested should count in favour of keeping it, and say what the analysis needs to price instead.
Check
The three years do not count in favour of anything; they are sunk and cannot be spent again regardless of the decision made today. The analysis needs to price the migration cost, what it would actually take to remove or replace the framework given how deeply it is wired in, against the cost already being paid every sprint by two of the five engineers, projected forward rather than backward. If the persistence code is isolated behind the team's own interfaces, migration cost is lower; if domain classes extend the framework's own base classes throughout, the cost is higher, and that is a fact to measure, not assume.
- ▢ A framework advertises "pluggable everything": several interchangeable database backends, several message queue backends, several serialisation formats. The service in front of you has exactly one of each in production, with no plan to change any of them. Predict which rubric question this fails, and why.
Hint
Ask what the smallest thing that would work for one database, one queue and one format actually looks like, and what it would be missing.
Check
It fails the first question, what problem the service actually has right now. The pluggability is a solution to a requirement nobody has, the classic shape of speculative generality: paying for abstraction now on the chance that the backends might change later, when nothing today calls for that flexibility and any current guess about what shape it would need is unlikely to match reality when the need actually arrives. A direct client for the one database and one queue actually in use would work today and would be missing nothing this service currently needs; the "pluggable" abstraction only earns its cost back on the day a backend actually gets swapped, which is not planned.
- ▢ Two engineers disagree about adopting a validation framework. One says "it is just objectively better code" and will not elaborate further. The other names the specific bug class it would prevent, the number of call sites it would touch, and offers to revisit the decision if the team stops spending time on that bug class for other reasons. Predict which argument can be revisited later if circumstances change, and which cannot, and say why.
Check
The second argument can be revisited, because it names the constraint that decided it, meaning the bug class and its cost, and states in advance what fact would change the conclusion, meaning the bug class no longer being a live problem. Anyone can check that fact later and act on it. The first argument cannot be revisited at all, because "objectively better" names no constraint and no condition that would change it; there is nothing underneath the preference to test against a new fact, so disagreeing with it later is just a second opinion against the first, not a check against evidence.
- ▢ A framework has been in a codebase for six years and is being removed because "everyone uses something else now." Nobody has priced the removal yet, and the codebase's business logic extends the framework's own base classes throughout rather than using them behind the team's own interfaces. Predict what discovering that fact should do to the removal's estimated cost, and name the earlier decision, from stage 2, that made this discovery expensive.
Check
The estimated cost should rise substantially, because every class that extends the framework's base class directly is a place the removal has to touch, and that count is now the whole codebase rather than one clean seam. The earlier decision is the one stage 2 warned about when it taught why composition usually beats extends and what extends actually costs: letting business logic inherit from a framework's classes rather than depend on it through the team's own boundary is exactly the choice that turns a six-year-old framework into something inseparable from the code it sits inside, and the exit cost this lesson asks you to price was fixed at that earlier moment, long before removal was ever on the table.
Real-world reps
- [ ] Pick one framework or major library already running in a service you own, and answer the exit-cost question for it honestly: what would removing it actually touch, counted rather than guessed.
- [ ] For the next dependency someone proposes adding to a service you work on, ask the smallest-thing-that-would-work question before the pull request is approved, and write down the specific gap it names.
- [ ] Find one framework-shaped decision in your own codebase's history, in a commit message, a changelog entry or an old ticket, and check whether the reasoning behind it was written down anywhere, or only the choice.
- [ ] Take a framework you already depend on and write, in one paragraph, the constraint that decided it, the cost you accepted, and the fact that would change your mind, as if explaining the call to someone joining the team tomorrow.
- [ ] Tomorrow: pick one dependency in a
pom.xmlyou can access and run the check lesson 33 taught, reading what it actually pulls in transitively, and ask whether every jar on that list is something the team chose or something that arrived uninvited.
Going further
- Yagni, Martin Fowler: the speculative-generality argument this lesson's rubric borrows for judging a framework adopted ahead of any actual requirement
- Judgment: the stage 7 reference sheet
- Resources
Not landing? Reread the primary source at the top, since this lesson compresses it and compression is where understanding leaks. Check the glossary for any term that felt slippery.
If the lesson itself is unclear rather than the material, that is a defect: open an issue.