The wrong question: 'ready-made or hand-written?'
This debate usually drifts somewhere ideological: one side says 'don't reinvent the wheel', the other says 'dependencies take you hostage'. Both are true and both answer the wrong question. The right question is: how close is this piece to the identity of our product, and what will it cost if we one day have to change it? The answer differs per piece, which is why you need a set of criteria rather than a single rule.
Our practice is mixed, and deliberately so. For broad-surface, mature, industry-standard pieces β the database, the server framework, the UI framework, the drag-and-drop kit, the internationalisation layer β we use what exists. Meanwhile we wrote our own rich-text editor, charts, table component and visit analytics. The difference is not 'hard versus easy'; it is that each has a different closeness to the product and a different cost of change.
First criterion: the size of the surface
The broader a piece's surface, the worse the idea of writing it yourself. Database engines, operating systems, browser frameworks β these are software shaped over decades and exposed to thousands of edge cases. Attempting to rewrite them means spending a year building your own infrastructure rather than your product. Our position is clear: core infrastructure is taken as it comes, and we do not open that debate.
For narrow-surface pieces the equation flips. Take a table component: what you need is sorting, paging, server-side filtering and row actions. A ready-made component gives you those, but it also brings its theming system, its own event model and thirty features you will never use. A narrow component you wrote speaks the same language as the rest of your product and bothers nobody on upgrade day.
Second criterion: closeness to the product's identity
The closer a piece is to what a user remembers about your product, the more sense it makes to write it yourself. In a product built around content management, the text editor is exactly that: the user spends most of their time with the product inside it. How the editor behaves, how it cleans pasted text, how it treats headings β these are not 'details', they are the product.
We wrote the editor on top of the browser's own editable area, taking no external component. Not because it was easy β on the contrary, it cost several extra weeks. In return we fully control paste behaviour, the permitted tag set, image placement and theming. The same editor runs in two of our products, and in neither is there a risk of an external version bump breaking it.
The same logic applied to charts. We produce the charts on our report screens with direct vector drawing rather than a charting library. The reason was simple: we needed three chart types, and all of them had to follow our theme's colour variables and behave correctly in both light and dark mode. We cannot claim that bending a library to our theme would have cost less than writing three charts.
Third criterion: licensing and ownership risk
A dependency's licence today is not its licence tomorrow. It is not rare for a component used freely for years to move to a commercial model, for a critical package to be abandoned, or for the new version to become paid. This is not a question of bad faith either β maintenance is done by a person, and that person has economic realities. But for your product the consequence is the same: an unplanned decision.
For us this happened with an object mapping library: because it moved to a commercial edition, one of our products faced the decision of 'buy a licence, stay on the old version, or write our own small solution'. None of the three is a catastrophe; but all three mean unplanned time. After that experience we added a question to our criteria: if this component became paid tomorrow, what would it cost us?
The practical benefit of that question is not to reject dependencies but to bound them. If you keep a component behind a single wrapper rather than sprinkling it through the code, swapping it becomes a day's work. Use it directly and unwrapped, and the same swap stretches over a month. We always keep critical dependencies behind an interface, so that if the source changes, only that layer changes.
Fourth criterion: the upgrade burden
The real cost of dependencies is not installation day but upgrade day. In a project using dozens of packages, every version bump produces a compatibility exercise; sometimes one package pins another to an old version and you are left with a knot that cannot be untied. That knot usually draws an invisible boundary: 'we cannot upgrade the framework because that package is not compatible yet'.
A component you wrote yourself never becomes part of that knot. When the UI framework moves up, you adapt your own editor or chart; you wait for nobody. Let us state its price honestly too: the adapting is yours to do. The burden does not vanish, it moves β from an indefinite wait imposed from outside into a work item you can plan.
The hidden cost of writing your own: edge cases
When you write your own component, the visible work is producing the version that runs on screen; the invisible work is the edge cases a library has accumulated over years. In a text editor they run like this: paste behaviour across browsers, the undo stack, keyboard shortcuts, screen-reader compatibility, right-to-left languages, mobile keyboards. You cannot solve them all in the first version, and you do not need to β but you do need to know the list.
Accessibility is the item most often skipped on that list. A ready-made component usually brings keyboard navigation, focus management and screen-reader labels with it; in a component you write, you have to think of those yourself. In enterprise and public-sector projects this is not a preference but frequently a requirement. Include the accessibility work in the cost when you decide to write the component.
Our method was to narrow the scope deliberately: our editor does not do everything, it does the limited tag set needed for writing content well. Our charts do not draw every chart type, they draw the three we need correctly. When writing your own component, the winning strategy is not imitating the library but stopping exactly at the edge of your need.
Community and documentation: the risk of being alone
The most valuable part of a ready component is not its code but the knowledge around it: question-and-answer archives, examples, issue trackers, hundreds of people who hit the same problem as you. Writing your own means giving up that entire network; when you search for a problem you hit, nobody answers, because that code exists only in your house.
The way to reduce that risk is taking documentation of your own components seriously. If how an internally written component is used, why its decisions were made and where its limits lie are not written down, then the day one person leaves the team, that component becomes untouchable territory. Your own code needs more documentation than an external library, not less.
Running the criteria on real decisions
We took the drag-and-drop kit ready-made. Reason: broad surface (touch events, scrolling, accessibility, browser differences), far from our identity (a user may say 'that drags nicely' but will not credit our code for it) and a mature option exists. Writing it ourselves would have cost weeks and gained nothing distinctive.
We wrote the table component ourselves. Reason: narrow surface (sorting, paging, server-side filtering, row actions), close to our identity (most screens are tables) and theme consistency is essential. Fitting a ready table onto our own theme variables was comparable work to writing one β and it would have repeated at every version bump.
We took the internationalisation layer ready-made but bound content translation to our own model. This is the third path we use most often: the infrastructure is ready, the product-specific part is ours. You are not obliged either to adopt a dependency wholly or to reject it wholly; most right decisions are about deciding where the line runs.
When do you decide to remove a dependency?
Removing a dependency is as much a decision as adopting one, and it is usually made too late. The signals are: you cannot upgrade the framework because of the component, the surface you use is a small fraction of the total, every release brings compatibility work, or there is uncertainty around licence or maintenance. When two of those four are true at once, it is time to discuss removal.
What makes removal easy is the wrapper you put in at the beginning. If the library is called directly from hundreds of files, removal becomes a refactoring project; if it sits behind a single layer, replacing it with your own code takes days. So the thought 'we might swap this one day' should be written into the architecture on the day you adopt the dependency.
Control everything that touches your data yourself
For components that touch user data the criteria change entirely: here the conversation is about responsibility, not convenience. Wiring up visit analytics with a ready external service is five minutes of work; but from that moment you are sending your visitors' data to a third party, and the legal weight of that is yours. That is why we wrote our own measurement: it collects nothing that identifies a person and sends it nowhere.
The same thinking applies to authentication. Instead of the common solutions we use a session token that lives server-side and can be revoked. The reason is need rather than fashion: in an enterprise product, 'cut this person's access now' is not theoretical but a frequent request, and a client-held token that cannot be revoked until it expires cannot satisfy it.
Security: the invisible side of the dependency chain
When you take a package into your project you do not take only that package; you take its dependencies, their dependencies, and the maintenance quality of everyone in that chain. In a modern front-end project the ten packages you added directly can become hundreds behind the scenes. A vulnerability arising in any link of that chain is your project's vulnerability, and most of the time you will not even know.
This is a reason to choose deliberately rather than to avoid dependencies. The practical measures are simple: keep the number of direct dependencies low, pin versions, run security scans at regular intervals, and notice abandoned packages early enough to replace them. Looking at when a package was last updated is the cheapest check you can make before adopting it.
Code you wrote stays outside that chain but is not automatically safe β responsibility simply moves entirely to you. The difference: with an external package you wait for a fix, with your own code you make one. Which is faster depends on your team's capacity; both need an answer.
The team-size criterion: how large must a team be to carry its own code?
The decision to write your own component must be a load the team can sustain. In a three-person team, writing your own editor, table and charts also means dumping their maintenance on those same three people. Sometimes that is the right call β it was for us β but only when those components sit in the product's identity and are kept narrow.
The criterion we use: we write a component if we can commit to updating it for the next three years. If we cannot, we take the ready-made one β we do not decide on the basis of 'this is more fun right now'. A team that writes its own code and cannot maintain it two years later is worse off than one that took a dependency: it carries code that is both old and known to nobody.
How the decision gets made: four questions
The four questions we ask before taking on a dependency: One, how broad is this piece's surface β something we could write in a week, or a decade of accumulated work? Two, how close is it to the product's identity β will the user remember this piece as 'your product'? Three, if its licence or maintenance changed tomorrow, what would it cost? Four, will it hold us back when the framework moves up?
The answers to those four usually point in a clear direction. A broad-surface piece, far from your identity, mature and well maintained: take it, use it, wrap it. A narrow-surface piece, close to your identity, demanding theme and behaviour consistency: write it. For those in between, make a provisional decision and keep it behind an interface β the cheapest way to postpone a decision is to make it reversible.
One warning to close with: deciding to write it yourself also means taking on that piece's maintenance. The editor you wrote will need updating next year, adapting to browser changes and receiving bug fixes. If you are not going to do that, using the ready-made one is the more honest decision. Writing your own code and then not maintaining it is worse than taking on a dependency.
Conclusion
Choosing dependencies is a job for ownership criteria, not engineering taste. Take broad-surface infrastructure as it comes; write the narrow, theme-sensitive pieces that carry your product's identity yourself; keep everything that touches your data under your own control; and put every dependency you do take behind an interface, so that swapping it one day is a day's work.
In our products these criteria show up in what is live: a library-free editor, library-free charts and our own analytics; but a ready database, server and UI framework. Framing the debate as 'which is right for this piece' rather than 'which is right' saves both time and nerves.