What a Framework Charges
Productivity, conventions and an ecosystem, in exchange for its lifecycle, its architecture, an upgrade obligation and the moments its abstractions leak. All four charges are payable later, which is why only the benefits are visible on day one.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
We are productive in this framework. What is it charging us, and when does the bill arrive?
A four-year-old service built on a full-stack framework needs to move to the framework's next major version, which changed how its ORM handles lazy loading and how middleware composes. Security patches for the current version end in five months.
The framework paid for itself in the first month. Upgrades are a maintenance task and get scheduled like any other.
The bill is not proportional to the benefit and does not arrive at the same time. The productivity is delivered in week one; the upgrade cost lands four years later, on a different team, against a deadline set by someone else's support policy.
- The bill is not proportional to the benefit and does not arrive at the same time. The productivity is delivered in week one; the upgrade cost lands four years later, on a different team, against a deadline set by someone else's support policy.
- The upgrade is not a version bump because the framework owns control flow. A change to how middleware composes changes when your code runs, and that is not something your test suite necessarily notices (Library or Framework).
- Four years of the framework's conventions have become the architecture: entities are the domain model, handlers hold rules, and lifecycle hooks hold effects. All of that is inside the inversion and all of it moves (Package by Layer).
- And the leaks are where the time actually goes. Lazy loading behaving differently is an abstraction leaking a persistence detail into business code that never mentioned persistence (Leaky Abstractions).
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- The service handles payments and cannot have a long freeze.
- The framework version is used by two other services with different upgrade appetites.
- Roughly two hundred handler classes, and an unknown number of places that rely on lazy loading working the way it currently does.
- Behaviour is unchanged by the upgrade: an upgrade that alters what customers are charged is not an upgrade (What Refactoring Actually Is).
- The service stays on a version that receives security patches.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- The framework owns the lifecycle, wiring and conventions — that is what was bought, and using it fully for those is correct.
- Your code owns the business rules, and owning them means keeping them where a framework upgrade cannot reach them.
- Someone must own the upgrade cadence as ongoing work rather than as an event, because the cost of skipping versions compounds (Dependency Management).
- The boundary that determines the upgrade cost is the one between framework-shaped code and everything else. Two hundred handlers that only map and delegate are a large but mechanical change; two hundred handlers containing rules are a rewrite (Boundary Adapters).
- Framework abstractions leak at predictable places — persistence, transactions, serialization, async — and those are the places worth an explicit seam even when the framework offers to handle them (Effect Boundaries).
The exchange, stated in full
Every row of this is real, in both columns. A framework adoption is not a mistake to be regretted; it is a purchase whose terms are worth reading, and the terms are almost never discussed at the moment of purchase because the benefits column is what the evaluation was about.
The asymmetry is timing. Everything on the left is delivered immediately and everything on the right is payable later — usually to different people, on a schedule you do not set.
| What it gives, immediately | What it charges, later | What decides how big the charge is |
|---|---|---|
| Productivity: routing, wiring, serialization and migrations that you did not write | An upgrade obligation on someone else's schedule, with a support window you do not control | Whether you upgrade continuously or in four-year events (Dependency Management) |
| Conventions: five engineers who agree where things go without a meeting | The conventions become the architecture, including the ones that were never right for your domain | Whether the domain model is expressed in the framework's types or in your own (Domain Modeling) |
| Ecosystem: a plugin for everything, and answers to your questions already written | Your upgrade is gated by the slowest plugin, and the ecosystem thins as the framework ages | How many plugins you depend on and how central each is (Transitive Dependencies) |
| Abstraction: persistence, transactions and async handled for you | The leaks — lazy loading, transaction scope, cancellation — surface as bugs in code that never mentioned persistence | Whether effects have an explicit boundary or are wherever the framework allows them (Effect Boundaries) |
| Onboarding: hire for the framework, not for your codebase | Nobody can reason about a handler without knowing the framework's lifecycle | How much behaviour sits inside hooks and decorators rather than in plain functions (Local Reasoning) |
The bill, four years later
A major version upgrade is where all four charges are collected at once, and the size of the bill was determined years earlier by a decision nobody framed as being about upgrades: whether the business rules live inside the framework's reach.
Both columns describe the same two hundred handler classes. The difference is not how many files change — it is whether changing them requires understanding what each one does.
Move a payments service to the next major version before security patches end in five months.
Every file has to be read, because every file contains behaviour. The tests boot the framework, so they change with it and cannot be trusted as a baseline. Lazy loading changing means every implicit fetch is a potential behaviour change in a service that moves money.
The same two hundred files change, mechanically and often by codemod, and the domain suite — which never booted the framework — is the evidence that nothing moved. Lazy loading is still a real problem, but it is contained in the persistence adapters where fetching is explicit.
Where the rules go to hide
The specific mechanism by which a framework ends up owning your business rules is worth naming, because it happens one convenient decision at a time and each decision is defensible.
A hook is offered, it is exactly where the data is, and putting the rule there saves threading a parameter. Four years later the rules are distributed across a lifecycle that the next major version is entitled to redefine.
looks like Domain behaviour in beforeSave, afterCommit, onSerialize or a request interceptor: a discount applied in an entity callback, an email sent after a transaction commits, a permission enforced in middleware and nowhere else. The domain code reads as though none of it happens, because from the domain's point of view none of it does.
suggests The rule was placed where the data happened to be available rather than where it belongs. It is now invisible to anyone reading the domain, untestable without the framework, and scheduled for renegotiation at the next major version (Hidden Global State).
fix Return the effect instead of performing it: the domain function produces a decision and an event, and the framework layer carries it out. That single move takes the rule out of the lifecycle, makes it testable without the framework, and takes it off the upgrade surface — at the cost of a parameter or a return value that has to be threaded through (Effect Boundaries).
How to build it
Most important first.
- Take the conventions and refuse the architecture. Use the routing, the wiring and the project layout; keep the domain model out of the framework's entity types (Domain Modeling).
- Upgrade continuously rather than in events. A minor version every month is boring; four years of accumulated majors is a project with a deadline set externally.
- Put business rules where the upgrade cannot reach: plain functions and types, tested without the framework running (Testing as Design Feedback).
- Treat lifecycle hooks as a place for framework concerns only. A rule in a hook is a rule the next major version gets to renegotiate (Hidden Global State).
- Before adopting, count what the framework claims: routing, persistence, validation, serialization, jobs, auth. Each claim is a future upgrade surface, and some frameworks claim far more than others (Build, Library, SaaS or Managed Service).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Rules inside handlers and entities: the upgrade touches two hundred files where each one has to be understood, not just mechanically changed, because each contains behaviour. Estimate is unreliable and the risk is behavioural.
- Rules outside: the upgrade touches the same two hundred files mechanically, and the domain tests — which never boot the framework — are the safety net proving nothing moved. Estimate is roughly linear and the risk is schedule, not correctness.
- What is unchanged in both: the lazy-loading leak. Every place that relied on an entity fetching related data on access has to be found and made explicit, and that is a persistence detail that leaked into business code years ago (N+1 as a Design Problem).
- Keeping the domain framework-free means mapping layers and hand-written translation that the framework would have done for free, and that code has to be maintained and reviewed.
- Refusing the framework's ORM entity as the domain model gives up a lot of genuine convenience — lazy relations, change tracking, migrations generated from types — and some teams will trade a hard upgrade for four years of that.
- Continuous upgrading spends a steady amount of time on work with no feature attached, and that is real cost, not just insurance. Some services are deleted before the bill ever arrives.
What can go wrong
- The upgrade is deferred repeatedly until the version is unsupported, at which point it is a security problem rather than an engineering one.
- The team upgrades by rewriting behaviour it did not understand, and the payment service starts charging slightly differently (Characterization Tests).
- The framework is abandoned upstream, and the ecosystem that was the reason to adopt it evaporates faster than the framework does.
- Overreaction: the team wraps every framework feature in its own abstraction, pays the framework's cost, gets none of its conventions, and now maintains a second framework (Over-Design and Under-Design).
- You depend on the framework's release cadence, its support window and its maintainers' decisions about breaking changes — none of which you influence (Deprecation).
- The ecosystem is a dependency multiplier: plugins are pinned to framework majors, so an upgrade is gated by the slowest plugin you use (Transitive Dependencies).
- Your team depends on the framework's conventions for shared understanding, which is the benefit — and it means a migration is also a retraining cost (Knowledge Sharing).
- "Frameworks are a trap." They are the reason four people can ship a service in a week with conventions they share. The charge is real and so is the value; the mistake is not knowing what was bought (Library or Framework).
- "Wrap everything and stay portable." Full portability means paying the framework's cost with none of its leverage, and portability is almost never exercised (Speculative Generality).
- "Our tests will catch upgrade problems." Only if they test behaviour rather than the framework's wiring. A suite of tests that boots the framework and asserts on its mechanics changes with the framework (Mocking).
- "We can skip to the newest version later." Skipping majors multiplies the change surface; upgrade cost is superlinear in versions skipped (Incremental Migration).
- hidden-global-state
- anemic-domain-model
Testing it, and how it ages
- Domain tests that never boot the framework are the asset that makes an upgrade tractable. Their existence is decided years before the upgrade (What a Unit Is).
- For the upgrade itself, characterization at the HTTP boundary: same requests, same responses, same database effects, across both versions (Characterization Tests).
- A test that fails if a framework type appears in a domain signature is cheap to write as a lint rule and prevents the slow drift that makes the next upgrade expensive (What to Automate Out of Review).
- Every framework has a lifecycle: adoption, dominance, maintenance, decline. Adopting near the start buys a long runway and unstable APIs; adopting late buys stability and a shorter remaining life (Stability and Dependency Direction).
- The parts of a codebase that survive framework migrations are the parts that never knew about it, which is the strongest practical argument for keeping the domain framework-free.
- Upgrade cost compounds with skipped versions, so a team's upgrade cadence in year one largely determines what year four costs.
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- FRAMEWORK-SPECIFICHow much is charged depends on how much the framework claims. One that owns only routing and wiring charges a small upgrade surface; one that owns persistence, validation, serialization and background work charges an upgrade that touches the data model. Counting the claims before adoption is the single most predictive thing you can do (Build, Library, SaaS or Managed Service).
- LIFETIME-SPECIFICFor a service with a two-year life the upgrade charge is never paid and the productivity is pure gain; frameworks are underrated for short-lived and internal software for exactly this reason. The charges here matter for code expected to outlive at least one major version.
- CONTESTEDThe strongest opposing view: the domain-free-of-framework discipline is a purity tax that has cost the industry more than framework upgrades ever have. Teams that use the framework fully — entities as the model, its validation as the rules — ship faster for years, hire more easily, and when the upgrade comes it is a mechanical grind that a large community has already documented and tooled. Against that, the honest counter is not portability but local reasoning: the cost is paid daily by everyone who cannot tell what a handler does without knowing the framework, not once at upgrade time (Local Reasoning).
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Programming Languages & Runtime Internals — how invisible a framework's control flow is depends on the language's metaprogramming: annotation and reflection-driven frameworks hide the call graph from every tool you own, including your ability to read it.