I usually don’t plan all the small details of an application up front. Certainly not listing all the messages and sub-models.
If I’m planning on the scope of a whole application, I’m thinking more at the product/design level. What problem is the application solving? What interactions does the app need to facilitate to solve those problems? What pages does the app need to create those interactions?
For the actual implementation details, I tend to take an iterative development approach. Elm’s compiler makes this particularly easy because it has my back when I make changes. Even big architectural things like “how are we going to structure pages?” and “how are we going to share layout code?” can start out dead simple and then evolve over time.
I do most of my planning is when I’m introducing types. This is where I do a lot of data modeling. I take time to understand what I’m modeling and try to make sure the type reflects that. In particular, I try to make impossible states impossible.
This can also be an iterative process. For example, I was working with our designer to build a filterable-dropdown input to choose a value from a giant list on the server. He’d built some mockups of the open and closed states. Based on some back and forth, we added a few more states. As I started implementing things in Elm, the type system started to push back against me and I realized we needed to handle loading, invalid, and network failure cases (yay type system! )
I went back to our designer and we discussed how to handle these states. After a few rounds of this, his mockup ended up kind of like @rupert’s state machine diagram: