When working on a large or potentially complex project, I look at the features and capabilities I need to implement and what problems they pose. Then I ask myself, "If there were already libraries or APIs that existed to solve these problems, what would they look like?"
Next, I proceed to write some examples using those APIs. I judge these imaginary APIs by how clear and easy to maintain they would make the code for my particular project, being sure to avoid APIs that are overly generic, and then the remainder of my time is spent making my chosen APIs a reality. It might take some time, but I find this is best way to break a project down into its smallest architectural components without having to rely on textbook examples or past domain experience, and without getting side-tracked by building yet another framework.
To implement my imagined APIs, I start by turning my examples into test cases. This approach is one of the few times I really enjoy using a test-driven development style.
From there, I tend to use a lot of exploratory, bottom-up programming and existing libraries and APIs to get something working quickly. This might result in a large, organically grown mess of code, or it may just be a light wrapper around an existing library I found.
However, by starting with the end in mind, I manage to create a cleaner API for describing and solving each of my problems. This results in writing code that is more portable, simpler to test, and easier to refactor or swap out.
So the next time you're faced with a difficult problem, and you don't know where to start, try starting with the end and working backwards.