The upload feature I shipped by deleting most of its design
We added a reason step with an optional PDF to our policy-cancellation wizard. My first upload design had two new endpoints, a new table, and a cleanup cron — what shipped has none of them. How walking the real user flow one more time made the feature smaller, safer, and done faster.

When we added a "reason" step to our policy-cancellation wizard — why are you cancelling, in your own words, plus an optional PDF — my first design for the file upload had two new endpoints, a new database table, a state machine for pending files, and a cleanup cron for orphans. What shipped has none of those things. This is the story of how questioning my own design made the feature smaller, safer, and done faster.
The step itself: chips that type for you
The reason step looks simple: three quick-pick chips ("Selling the car", "Switching insurer", "Other"), a free-text field, and a document upload. The first version treated chips as hidden metadata — you tapped one, it got highlighted, and its label was silently attached to the request at submit time.
My teammate's feedback changed my mind about that. If the chip's text ends up in the request, the user should see it. So now tapping a chip physically inserts its label into the comment field, where you can edit it, delete it, or build on it. Deselecting removes exactly the phrase it inserted — and only if you haven't edited it away, because your words belong to you. What you see in the textarea is byte-for-byte what our staff receives. No invisible payloads.
The upload design I almost built
The PDF is attached two steps before the cancellation record exists. My first instinct was the "proper" architecture: upload eagerly the moment the user drops the file, return a document id, carry it through the wizard, link it at submit. That needs an upload endpoint, a delete endpoint for the "×" button, a table with pending/linked states, ownership checks, an anti-abuse cap, and a daily cron sweeping abandoned files.
Then my teammate asked a question that changed the plan: we already have APIs for calculation, saving, and status — can we be more pragmatic?
Walking the real flow gave the answer. The document is optional. It's a single file. And the save endpoint already persists the full request into our web app's own database. Every piece of that eager-upload machinery existed to manage server-side state before the cancellation exists — but nothing in our requirements actually needed server-side state that early.
Files that live in RAM until they matter
The shipped design: the attached PDF lives in browser memory as a File object until the final submit. The "×" button? It clears a variable — zero network calls, zero server garbage to clean. Navigating between steps keeps it alive because the whole wizard is one component. When the user finally submits, the same save request that always existed goes out as multipart/form-data with one extra part: the file. No file attached — the request stays the exact JSON it was before, byte for byte.
One request in, three columns out. The backend validates the PDF before calling the insurance core (a broken file dies while nothing exists yet), then stores the bytes in our S3 bucket and writes just a pointer — key, original name, size — onto the same database row it was already creating. The insurance core never hears about the file at all.
The details that earn the trust
A refund request must never fail because of its optional attachment — so the S3 write is deliberately best-effort. If storage hiccups after the cancellation is created, the response still succeeds and carries document_uploaded: false so nothing fails silently. The server verifies the file is a real PDF by reading its actual bytes, not the content-type the browser claims. And our HTTP client's default 15-second timeout — fine for JSON — got a 60-second override on the multipart branch, because 10 MB on weak mobile internet deserves patience.
Before anyone tested it live, I validated the wiring with mock tests at every layer: the request builder against a stub (13 checks — JSON stays JSON, FormData carries every field plus the file), and the backend's actual validation class against a real PDF, a text file renamed to .pdf, an oversized file, and a doubled part. All green, and not a single real cancellation created in the process.
What I keep from this one
The eager-upload architecture wasn't wrong — it's the right shape for multi-file, mandatory documents with independent retry. It was wrong for these requirements. The lesson I keep re-learning: when a design grows endpoints, tables, and cron jobs, walk the actual user flow once more and ask what each piece protects. Sometimes the most senior thing you can do is delete most of your own design before anyone builds it.