Two dependency decisions on the same project landed in opposite directions within a few weeks of each other. One package got rejected before I wrote a line of code against it. Another got added, used successfully for weeks, then partially reverted after it broke the build for reasons that had nothing to do with anything I'd written. Both were correct calls, and the reasoning behind each is more useful than either outcome on its own.
The one I didn't reach for
The email-sync engine in the app I'm rewriting my own infrastructure as needs a MIME parser: something that turns raw RFC 5322 bytes into headers, body, and attachments. Before writing one, the honest first question is whether a package already solves it well enough. For Swift specifically, at the time I looked, the realistic options were small, thin wrappers with limited real-world mileage, not a mature, battle-tested library with years of messy production mail behind it the way something like Python's email package has.
That gap, small and unproven against messy real mail, is a specific, falsifiable reason to build instead of depend, not a generic "I'd rather write it myself" instinct. A thin, lightly-used MIME library is a bet that its author already hit the same edge cases real mail servers produce: malformed encoded-word headers, inconsistent multipart boundaries, RFC 2231 continuation parameters, quoted-printable bodies with encoding quirks. A library with limited real-world exposure hasn't necessarily hit those yet, which means the bugs are still ahead of you, just hidden behind someone else's abstraction where they're harder to find and you can't fix them without forking. I built the parser in-house instead, validated it against 3,844 real production emails the way I wrote about in testing parsers against real data, and it found its own share of edge cases directly, where I could see and fix them, rather than inheriting an unknown set from a dependency I didn't control.
The one I did, and then partly walked back
S3 access is a different story. There, a mature, well-used Swift package, Soto, exists specifically for AWS-compatible object storage, and reimplementing SigV4 request signing and the S3 API surface by hand would have been exactly the kind of low-payoff reinvention the MIME decision was trying to avoid in the other direction. Soto went in, resolved cleanly, and a first real build against it came back clean. The right call, made for the mirror-image reason of the MIME call: this is a well-trodden, well-tested surface, and depending on it saves real time without inheriting a thin abstraction's undiscovered bugs.
The complication showed up in testing, not in the app itself. Soto ships a companion package, its own test-server utilities, meant to let you write real request and response round-trip tests without hitting a live endpoint. Adding it to the test target's dependency graph broke the build, not with an error in my code, but with a linking failure: an optional tracing-middleware feature inside Soto's core package failed to resolve its symbols specifically under Xcode's per-package dynamic-framework test mode, a failure mode that only appears once that particular combination of package traits and Xcode's own build configuration for test targets is in play.
Confirming it wasn't a mistake on my side
The instinct when a dependency breaks your build is to assume you configured something wrong, and that instinct is usually right, so it's worth ruling out carefully before you conclude otherwise. I went through the actual options: explicit product dependencies on the specific pieces that seemed related, different combinations of how the test target declared its relationship to the package, checking whether the framework itself, independent of anything this project's target declared, ever linked the symbols the linker was complaining about missing. It didn't, in any configuration. The missing symbols weren't something this project's dependency graph could produce regardless of how it was declared, which meant the gap lived inside the published package, not in how I was consuming it.
That distinction matters for what you do next. A dependency problem you caused, you fix by changing your own configuration. A dependency problem the package itself has, you can't fix from your side at all, and continuing to try is a way to spend an afternoon proving a negative you already have enough evidence for.
Reverting the part that broke, keeping the part that worked
The resolution wasn't to drop Soto. The S3 client itself worked fine; the break was isolated entirely to the optional test-utilities companion package. So that one dependency came back out, cleanly, and the test coverage that would have used it got rebuilt around what's actually provable without a live or mocked endpoint: local input validation, which runs before any network call happens at all, and the presign logic, which is pure, local computation you can verify completely offline with no server involved on either end. What's left genuinely untested at the unit level, live list, upload, download, and delete round trips against a real bucket, gets deferred to manual verification once real credentials exist, which is an honest gap to name rather than paper over with a broken dependency kept around for appearances.
The actual decision rule
Neither of these was "avoid dependencies" or "always use dependencies." Both were the same question, asked honestly each time: does this specific package, on this specific surface, know something I don't and would take real time to learn myself, or does it know less than the surface actually requires. Soto's core knows the S3 API and SigV4 signing better than a hand-rolled implementation would have, cheaply. A thin, unproven MIME library didn't clearly know more about real-world mail malformation than the RFC itself does. And when a package breaks for reasons that trace back to the package, not your use of it, the right response isn't stubbornness in either direction, keep fighting it or rip out everything it touched. It's isolating exactly what's actually failing, confirming the fault lives where you think it does, and keeping every part that's still pulling its weight.