Competent engineering is straightforward. Great engineering is uncomfortable, and I mostly find that out by failing at it.
I've shipped plenty of competent work. It closes the ticket, passes CI and lets everyone log off at five: a clean React app that renders fast, a feature that matches the Figma file, a schema in third normal form. Nobody argues with it in review, it ships on time, and I'm not knocking it.
Great is the other thing. An interface so close to how people behave that nobody opens the docs. An abstraction that deletes a whole class of bugs for everyone else. A caching rewrite that changes how fast the entire product feels.
Nowhere to hide
Aiming for competent comes with excuses built in: the framework, the deadline, the client. Aiming for great takes them away. If the abstraction fails, it's because my model of the problem wasn't good enough, which is a much less comfortable sentence to say out loud.
You can't schedule it either. Nobody drags "be great" into a sprint. The best codebases I've read came from people stubbornly obsessed with one painful bottleneck, and the greatness turned up as a side effect.
What I do instead
So I've stopped aiming for great and started working on the conditions for it. I learn the domain properly and expect to throw away the first few attempts. I build for the users in front of me rather than this week's Hacker News thread, and I guard my focus, because deep work doesn't survive a chatty afternoon.
My machine is full of abandoned branches. Some ate weeks before showing a basic flaw. Others were abstractions too clever for anyone to maintain, refactored back into plain, competent code. That's the cost, and the instinct for great later comes from the branches you delete now.
If the gap feels huge, lower the stakes. Write something structurally terrible on purpose, just to explore.
Competence keeps you employed. Chasing great, even into a deleted branch, keeps the work from turning into typing.