I avoid over- or under-engineering by trying to balance the amount of time I spend investigating and designing with what I can reasonably expect its use and lifetime to be.
Let's say I'm writing a utility that only I will ever use, and I will use it rarely or even just once. If I have a choice between coding up such a utility in Bourne shell or Perl in ten minutes that takes overnight to run, and taking three hours to write an optimized version using sophisticated and difficult algorithms in C++ that runs in one minute... electricity is cheaper than my time.
On the other hand, I've written key components of products that have been used by or affected millions of people over many years. In such instances, it's been worth it to take the time and effort to put a lot of effort into investigation, research, and design, use the absolutely best tools and techniques, and then polish the resulting code to a glossy shine.
There's no set-in-stone way to do this - it's all judgment. And as with all matters of judgment, experience is very helpful, as Aaron Digulla aptly pointed out.
When I started out as a professional software developer twenty-four years ago, I didn't know jack about how to make these decisions. I'd just write code, and if it was too slow or too buggy or something, I'd go back and fix it. But when I did that fix, I'd try to think about how I could have avoided the problem in the first place, and how I could apply that in the future. And I also have tried to listen to other programmers when they talk about problems they ran into and how they fixed them.
Now, many dozens of projects and perhaps millions of lines of code later, there are a lot of design decisions I can make almost instinctively. For instance: "If you're working in C++, and you're facing a problem that some STL template will solve, and you're not constrained to avoid using STL, then that's the way to go. That's because modern STL implementations are highly optimized, and any improvement you could get from writing your own code would just not be worth the effort."
Also, I can just look at a situation and say, "The 10% of the project where we're likely to have problems is here, and here, and here, so that's where we need to concentrate our research and design effort. And the other 90%, let's just make it work however we can do it." And it works out pretty well.
So keep coding, and keep improving your code, and keep learning from other software developers and from your own experience. If you continue paying attention and thinking about things, increasing software design mastery will come over time.