The meeting where we agreed to replace our file uploader ran about twenty minutes, and a good part of it was nobody wanting to speak first. Nine days to a release, a support queue with uploads sitting at the top of it, and two options on the whiteboard that both looked bad.
“Swapping a file uploader mid-project is survivable if you isolate the uploader behind an interface first: same events in, same file URLs out. Our swap took nine days, and seven of them were untangling code that talked to the old uploader directly. The two days of actual integration were the easy part.”
That ratio is the whole lesson. The tool was never the hard part.
Key Takeaways
- Isolating your uploader behind a small adapter interface makes a vendor swap safe, even mid-project.
- Our nine-day swap split into seven days of untangling direct SDK references and two days of integration.
- An adapter contract needs only progress, a per-file success event carrying a URL, and a per-file error.
- Chunked transfer with automatic retries removes the large-file failures that send users back to zero.
- The migration deleted about 1,900 lines of custom upload code, making the next vendor change a two-day job.
Why We Had to Swap
Our uploads had a threshold, somewhere around a quarter of a gigabyte, past which finishing became a coin flip. The library we were using kept no record of what had already crossed the wire, so a dropped connection meant starting over at zero. A customer with a 1.4 GB asset bundle on hotel wifi did not have a slow feature. They had no feature.
Support saw it before engineering did. Uploads went from an occasional tag to the top of the queue in about three weeks, and the tickets all said the same thing: it reached sixty percent, it stopped, it started again from the beginning. One agency account retried the same bundle across two evenings before writing in to ask whether they were doing something wrong.
The whiteboard math was short. Patching meant writing resumable chunking and retry handling ourselves, against a library with no concept of either, and the honest estimate was two weeks with a tail we could not size. Swapping meant an unknown integration cost against a tool that already solved the problem. We took the unknown we could put a number on by Friday.
The Interface We Should Have Had From Day One
So we went looking for every place the uploader was called, and that is where the project changed shape. Fourteen files imported the vendor SDK directly. Two reducers stored its response object more or less raw. A notification component had a switch statement over its error codes. Nothing in our application talked about uploading. Everything talked about that specific uploader.
What we needed, and should have written in the first week, was an adapter. One module the rest of the app calls, with a contract narrow enough to survive a change of vendor. It is the ordinary adapter pattern, applied to a dependency most teams never think of as swappable. Events in. File URLs out. Callback shapes, retry state and vendor error codes stop at that boundary.

Before, fourteen files imported the vendor SDK. After, one module did.
Writing the contract forced a question we had never properly answered, which is what a reliable file upload service actually has to guarantee on the other side of it. We landed on two things. A success or failure event for every file, and a durable URL for anything that finished. Chunk size, retry policy, storage and delivery belong to the vendor by definition, and the app is better off never knowing about any of them.
Before: the component knows the vendor
import { LegacyUploader } from ‘legacy-upload-sdk’;
function UploadPanel({ projectId }) {
const onSelect = (files) =>
LegacyUploader.send(files, {
progress: (e) => setPct(e.loaded / e.total),
complete: (r) => attachToProject(projectId, r.storage_key, r.mime),
failed: (e) => toast(ERROR_MAP[e.code] ?? “Upload failed”),
});
// …
}
After: the component knows the contract
import { uploader } from ‘@/lib/uploadAdapter’;
function UploadPanel({ projectId }) {
const onSelect = (files) =>
uploader.upload(files, {
onProgress: (pct) => setPct(pct),
onFileDone: ({ url, mimeType }) => attachToProject(projectId, url, mimeType),
onFileError: (message) => toast(message),
});
// …
}
// uploadAdapter.js is the only module allowed to import a vendor SDK.
The adapter interface reduced the swap to one file. There is a lint rule enforcing that now, added the week after we finished, for obvious reasons. It also made the flow testable for the first time. A fake uploader that resolves and rejects on command took an hour to write, and it brought back a set of upload tests that had been quietly skipped for a year because running them meant hitting the real SDK.
Shortlisting Under Time Pressure
One afternoon is not enough time to be thorough, but it is enough time to be strict. Two practical questions framed it. Which upload widget actually behaves in a React app without dragging its own UI framework along, and what should a small team weigh when choosing a file upload API with a deadline already on the calendar?
Three criteria, in that order. Integration hours, measured by integrating rather than by reading a quickstart and feeling optimistic, with a ninety-minute timebox per candidate against the adapter we had just written. Documented retry behavior for interrupted transfers, in the documentation, not in a forum reply from 2021. And a published size ceiling, so we could stop discovering our limits in production.
That filter cleared the field quickly. Anything requiring a signing endpoint we had no time to build was out. Anything whose retry story amounted to “the browser handles it” was out. Two candidates survived the afternoon.
The Nine Days, Day by Day
We shortlisted two options and ended up on a managed file uploader, mostly for two properties that suited the seam we had just built. Chunked transfer with automatic retries removed the large-file failures that started this whole conversation, and it did that with the exponential backoff behavior you want rather than a fixed retry loop. Per-file success and failure callbacks mapped one to one onto our existing event bus, so the adapter renamed things and passed them along. That was the entire translation layer.
There was a bonus nobody had planned for. The picker ships with more than twenty ingestion sources, which let us delete three integrations we had hand-rolled for cloud-storage imports. They had nothing to do with the failure we were fixing. They were simply tangled into the same files.
Nine days, roughly as they happened

Seven days on coupling, two days on the swap itself.
| Day | What actually happened |
|---|---|
| Day 1 | Grep the codebase for every direct reference to the old uploader’s SDK. Fourteen files. |
| Day 2 | Write the adapter contract and route the main upload flow through it. |
| Day 3 | Rewrite the remaining call sites; stop storing vendor response objects in shared state. |
| Day 4 | Rework event handlers built around the old callback signatures. |
| Day 5 | Normalize error handling so components read messages instead of vendor error codes. |
| Day 6 | Build a fake uploader and backfill the upload tests that had been skipped. |
| Day 7 | Last sweep for stray imports; freeze the adapter contract. |
| Day 8 | Drop the new uploader in behind the adapter, behind a feature flag. |
| Day 9 | Ship to 5% of traffic, verify retry behavior on large files, open the flag. |
Alt text for web publication: “Timeline of swapping a file uploader mid-project across nine days.”
Days one through seven were untangling, not integrating. Tracing direct references, rewriting call sites, pulling vendor response shapes out of shared state. That work has nothing to demo. Standup that week was six variations of “still on the call sites,” which is a hard thing to say out loud with a release date approaching.
On day eight the new uploader went in behind the adapter and behind a feature flag, and the diff was small enough to review over coffee. On day nine we sent five percent of traffic through it, watched a 1.2 GB upload survive a connection we dropped on purpose, and opened the flag the rest of the way that afternoon. Two days, because by then there was somewhere to plug in.
What I Would Tell Past Me
Write the seam on day one, even if you never swap anything. It costs an afternoon, and what it buys is the ability to change your mind later, which is the resource you actually run out of near a deadline. Vendors change pricing. APIs get deprecated. A tool you chose carefully turns out to handle every case except the one your biggest customer needs.
The other thing I would say is that most teams ask the wrong question first. It is not which uploader to use. Developers at small companies want the easiest way to give their users a working file uploader, and the easiest one is whichever your app never learns the internals of. Choose something that fits behind a narrow interface, write the interface first, and the vendor becomes a decision you can revisit on a Tuesday instead of a nine-day emergency.
The Swap Was the Easy Part
The migration cost was coupling, not the tool. Seven of the nine days went to undoing choices that had nothing to do with uploading files and everything to do with how far one SDK had spread while nobody was keeping track.
The seam is the insurance. When it was finished we deleted about 1,900 lines of custom upload code: retry logic, chunk bookkeeping, a year of edge-case patches, and a queue implementation everyone had been afraid to touch. That is the number I think about now. Not the nine days. The 1,900 lines that only ever existed because we never drew a line between our application and somebody else’s SDK.
Frequently Asked Questions
Is it safe to swap a file uploader mid-project?
Yes, provided uploader access is isolated behind an interface first. The integration itself is quick. Untangling code that reaches into the old tool is what consumes the calendar.
What took the most time?
Coupling. Seven of nine days went to removing direct references to the old SDK. Integrating the replacement took two.
What made the new integration fast?
Per-file callbacks and returned CDN URLs that matched the event model we already had, so the adapter needed almost no logic of its own.
What does the adapter interface need to expose?
Very little. Progress, a success event per file carrying a URL, and a failure event per file carrying a message. Anything more vendor-specific than that belongs on the other side of the boundary.



