Opening the book…
Every abstraction adds a layer someone must learn, maintain, and see through when debugging. The promise is that it hides complexity, but a weak abstraction hides the wrong things and leaks the rest, so you pay the tax without getting the shelter. An abstraction earns its cost only when it removes more complexity than it introduces.
Start with the concrete, duplicated code and let the abstraction emerge once you can see the real shape. Prefer a well-named function over a framework, and a parameter over a plugin system. When you reach for an interface, ask what varies today, not what might vary someday. If you cannot name what it saves, delete it and inline the code.
Some boundaries are worth building ahead of need: a stable seam at a module or service edge, or an interface around a genuinely volatile third-party dependency. Buy insurance when the cost of retrofitting later is clearly high.