I often talk about dependency management from a security or build perspective, but there is another dimension that is rarely discussed until it’s too late: Invasiveness.
We’ve all been there. You pick a shiny new UI library or a robust backend framework. It speeds up development by 10x in the first month. But two years later, you need to upgrade, switch design systems, or migrate to a new platform, and you realize that library isn’t just in your code—it is your code. It has woven itself into every view, every service, and every test.
This is what I call an “Invasive Library.” These are dependencies like frontend component libraries (Material UI, Mantine, AntD), CSS frameworks (Tailwind), or backend frameworks (Spring, NestJS) that demand to be imported in almost every file you write.
In this post, I want to explore how we can balance the incredible velocity these tools provide with the long-term risks of Vendor Lock-in, and how to structure our code to minimize the “Blast Radius” of inevitable changes.
The Cost of Convenience
“Invasive” does not mean “bad.” I use invasive libraries in almost every project I touch. Spring Boot is invasive. Tailwind is invasive. React is invasive. They are powerful because they take over the heavy lifting, allowing us to focus on business logic.
The trade-off we make is simple: Short-term Velocity vs. Long-term Agility.
Vendor Lock-in
When you weave a library into 500 different files, you generally aren’t just using a tool; you are entering a marriage. If that vendor or maintainer decides to change direction, stop support, or introduce massive breaking changes, you are locked in.
I have seen teams stuck on ancient versions of component libraries because the “upgrade path” required touching 2,000 files. They became paralyzed by Vendor Lock-in. The cost of staying current exceeded the budget for the quarter, so the technical debt just piled up.
The Blast Radius
This brings me to a concept I call the Blast Radius.
- Low Blast Radius: A library that serves a purpose and is localized to a small set of classes. If it breaks, you fix a couple files.
- High Blast Radius: A library that needs to be weaved through almost everything. If it needs to be swapped, you are refactoring your entire codebase.
Handling invasive libraries is about containing or choosing the blast radius you and the team are ok with. Sometimes the library you choose up front requires full commitment. Other times, you can isolate it.
Strategies for the Backend
When dealing with backend frameworks like Spring or NestJS, there is no one-size-fits-all solution. You have a spectrum of choices ranging from strict isolation to full acceptance.
Option 1: Strict Separation (Hexagonal Architecture)
For teams that prioritize absolute portability or want to enforce a strict boundary between “Business Logic” and “Infrastructure,” Hexagonal Architecture (or Ports and Adapters) is the gold standard.
In this model, your Domain core has zero dependencies on the framework.
- Pros: You can swap frameworks easily. Your business logic is pure Java/Kotlin/TypeScript.
- Cons: It requires a lot of boilerplate code (mappers, adapters, DTOs). You often end up fighting the framework rather than leveraging its features.
Option 2: Strategic Commitment (My Preference)
Personally, I find that for your Core Framework (like Spring Boot), it often makes more sense to commit rather than resist. If you have done your due diligence and selected a framework as a long-term strategy, you should embrace it. Spring provides massive productivity/velocity gains (Security, Data, Transactions) that you lose if you try to abstract them all away.
The caveat: Commitment requires research. You are marrying this framework. Ensure it has a healthy community and a roadmap that aligns with your organization’s longevity.
Option 3: Selective Isolation (Facades for the rest)
While I advocate committing to the Core Framework, I act very differently with secondary invasive libraries. Things like PDF generation libraries, Excel exporters, or specific HTTP clients are prime candidates for the Facade Pattern.
If you use a specific PDF library directly in 15 different services, you are in trouble when that library is deprecated. Instead, wrap it:
public interface DocumentGenerator {
byte[] createInvoice(Order order);
}
@Service
public class PdfBoxGenerator implements DocumentGenerator {
// The ONLY place specifically coupled to Apache PDFBox
public byte[] createInvoice(Order order) { ... }
}
Now, your 15 services rely on DocumentGenerator. If you switch from PDFBox to IText, you change strictly one file. This is the same “Blast Radius” reduction strategy we use on the frontend.
The silent killer: API Leakage
A common pitfall I see when teams attempt this isolation is Data Leakage. You might wrap the logic in a service, but if your method signature looks like this, you have not accomplished your goal:
// BAD: Leaking the library's internal type (PDDocument) out of the facade
public PDDocument createInvoice(Order order); Even though the logic is hidden, every consumer of this service must now import org.apache.pdfbox.pdmodel.PDDocument. The library has leaked through your abstraction. To truly isolate an invasive library, you must map inputs and outputs to your domain objects (POJOs) or standard types (byte[], File, InputStream).
Strategies for the Frontend: The Facade Pattern
The frontend is where invasive libraries arguably cause the most pain. Component libraries (Material UI, Bootstrap, AntD) are notorious for this.
Imagine you use a <Button> component from a library.
// Used in 500 files
import { Button } from 'awesome-ui-lib';
<Button variant="primary" size="large" onClick={...}>Submit</Button> If awesome-ui-lib releases v2.0 and changes variant=”primary” to color=”blue”, you now have a refactoring task across 500 files.
The Mitigation: Wrappers (Facades)
The strategy I advocate for is creating your own component library that wraps the external one. You create a simple facade.
src/components/AppButton.tsx:
import { Button as LibButton } from 'awesome-ui-lib';
interface Props {
onClick: () => void;
label: string;
}
export const AppButton = ({ onClick, label }: Props) => {
// We translate OUR domain concepts to the library's API
return <LibButton variant="primary" onClick={onClick}>{label}</LibButton>;
};
Now, your application only depends on AppButton. If the external library changes its API, or if you decide to swap Material UI for Ant Design, the Blast Radius is limited to one file: AppButton.tsx. There can be a fine line to this strategy where you may not want to overconstrain your facades. You may want to let the library props through but focus leveraging your api as much a possible.
The Hidden Casualty: Testing
One of the most overlooked arguments for this isolation is Test Velocity.
When you directly use invasive libraries, writing unit tests becomes a chore of mocking complex third-party internals. “Why do I need to mock an HttpServletRequest or a specific generic Page<T> object just to test my business logic?”
By using the Facade pattern (like the DocumentGenerator above), your business logic tests change. You no longer mock the confusing internals of the PDFBox library; you simply mock your own clean interface:
when(documentGenerator.createInvoice(any(Order.class))).thenReturn(new byte[0]);
This decoupling keeps your test quite fast and resilient. If the library changes, your business logic tests don’t break—only the integration test for the Facade does.
Handling Tailwind
Tailwind is interesting because it doesn’t use imports, it uses strings (class=”p-4 bg-blue-500″). It is incredibly invasive visually and to the UI files. The strategy here reinforces the Component Facade above.
Instead of copying class=”bg-blue-500 text-white font-bold py-2 px-4 rounded” onto every button in your app, you should be relying on your AppButton component. By doing so, you define that long Tailwind string exactly once. If you decide to switch to a different CSS framework or just change the radius of your buttons, you change it in one file, not 500. You can also create utility functions or constants for common class combinations to further reduce duplication.
Pragmatic Acceptance
All this said, we must be pragmatic. Trying to abstract everything leads to “Architecture Astronaut” syndrome, where you build layers upon layers that solve no real problem.
Some lock-in is acceptable.
- Standard Libraries: I rarely wrap logging libraries like SLF4J. It is an industry standard. The likelihood of it disappearing or breaking drastically is low.
- The Core Framework: Trying to abstract away React itself (e.g., trying to hide hooks behind a generic interface) is usually a losing battle. You chose React, React Native, Spring; embrace it.
Conclusion
The goal isn’t to avoid dependencies—it is to be intentional about them.
When you pull in a library that will touch every file in your project, pause and ask: “What is the blast radius if I need to change this?” If the answer is “The entire application,” it might be worth spending a few hours building a facade or an architectural boundary. That small investment today protects your velocity tomorrow.
Disclaimer
This article was developed with the assistance of artificial intelligence tools to support drafting, editing, and clarity. The core ideas, structural planning, and technical insights reflect the original thinking and professional experience of the RBA consultant who authored the piece. AI was used as a productivity aid, while all concepts, recommendations, and perspectives remain the author’s responsibility.
About the Author
Adam Utsch
Senior Principal Consultant
Adam is a seasoned software professional with deep experience in development, deployment, and application support. With a strong engineering foundation, they specialize in building scalable solutions and mentoring others in the technologies that drive real impact. Adam is passionate about continuous improvement, collaboration, and staying ahead of the tech curve.