Matt Trout asked people to write about how they learned to design programs. Design is a skill largely untaught; I suspect other responses will suggest informal and inductive processes.
The Nascent Hacker and Design
My first encounter with programming was with personal computer BASIC in the early '80s. I wanted to play games, but the school system discouraged students from playing games during class time. Programming was fine. Thus, I taught myself to write games.
I returned to programming in the late '90s as a hobby. My main work was little challenge with plenty of spare time, so I could explore anything technical I wanted if I could justify it for business purposes.
I read and I experimented and I tried to answer the questions of other novices and I learned how to program.
My first few public free software projects went unused and unlamented. I worked on them to satisfy my desire to explore interesting technical challenges. They didn't meet real needs for me or anyone else. The problem with this approach is that it pursues novelty for the sake of novelty. The appearance of a better or newer novelty can undercut the motivation for the previous project.
In the decade since then, I believe I've learned a few things about how to design software. The most important lesson is build only what you need. Some people call this the YAGNI principle. That's effective for coding, but there's a different principle for design.
Necessity and Creativity
I believe that constraints improve creativity. I've participated in National Novel Writing Month, where the goal is to write 50,000 words of the first draft of a novel in 30 calendar days. The result is quantity, not necessarily quality, but the time constraint forces people to produce results.
It's taken almost two years to turn the results into a publishable novel, but the process worked for what it did.
Richard Gabriel's Worse is Better essay suggests similar results in software. A system that demonstrably meets the most important needs in an effective (if inelegant) way and works today is better than a system that theoretically meets all perceived needs in a breathtaking way but may not be available yet.
It's easy to gild the lily, to write software that does too much in an attempt to be all things to all people — but I believe that a constraint of delivery date or tight scope or available resources can help focus the project on its most important characteristics.
Experienced Hacker and Design
I've written about vision, especially in the context of Perl 5. I tend to let the vision for a project guide its design in the large. For example, my vision for Perl 5 is a language that scales from novices learning the language piecemeal to experts writing powerful, elegant programs with as few barriers and missteps and traps as possible. This suggests criteria to evaluate potential designs in terms of clarity, expressivity, and learnability — if not safety.
Another component of equal importance is necessity. Some of the earlier code in Parrot code is difficult to maintain due to (I believe) a premature focus on optimization. We've born the cost of maintaining (and testing and debugging and fixing and working around) several design decisions implemented because someone or other at some point thought that certain programs might run faster.
We've spent much time renovating, removing, and replacing some of these components.
We see a constant push and pull between high-level languages (the "customers" of Parrot, in one sense) and Parrot itself. Which features should Parrot support? What's the most elegant way to solve a problem in an HLL? Which solutions work better in Parrot? Which design decision will best suit multiple interoperable languages hosted in the same Parrot memory space?
I believe we can make better decisions now in part because we have actual need — HLL implementors with HLL implementations making their own design decisions based upon specifications and specification tests and input from language designers — driving the requests. I can profile a sample Rakudo application and find portions of Parrot that need optimization based on the codepaths real-world code executes. (That's why we can speed up Rakudo 2% here, 4.5% there, 6% there, and 31% there.)
Smart people in other fields sometimes call this the last responsible moment. That idea comes from manufacturing, but if anything it applies more to software, where everything but the laws of physics and monads are malleable, given time and will and design. Change happens. Allow it.
I believe designing to current, actual, perceivable, and measurable needs — if you understand those needs — produces better programs.