Not a technical trick but this helps immensely when designing an application: always start with good problem decomposition and draft the technical components and their interfaces derived directly from that.

What I realized over my career is that almost always the software architecture quality just follows how well the problem was decomposed initially. If that decomposing processing is done well, getting to good technical and visual interfaces for the application is a straightforward process.

Let me give a practical example of good decomposition and translation to technical interface.

Let's say you want to develop an application to manage a parking lot. Start by listing what happens in the parking lot, thinking in a story format (user stories are not a coincidence):

- A car enters, the machine generates a code and prints into a ticket, the driver picks the ticket up

- Then the driver searches a suitable spot and parks the car

- Afterwards, the driver inserts the ticket in a machine that reads the code, calculates the price and ask for payment

- The driver pays, the machine returns the ticket and the driver proceed to exit

- At the exit, the driver inserts the ticket on another machine which validates if it was paid before opening the boom pole

Now, let's draft the components and their interface for that. Tip: the best names are already contained on that story. We need to:
1. Generate ticket code
2. Calculate price for ticket
3. Request ticket payment
4. Validate ticket

We can easily translate those to something similar to this:

TicketManager.generateTicket() => ticketCode

TicketManager.calculateTicketPrice(ticketCode) => float

TicketManager.requestTicketPayment(ticketCode) => (integration with the machine)

TicketManager.validateTicket(ticketCode) => boolean

Here is a good start point, you can decide to put those methods and their implementation into specific components depending on how complex those are. Yes, everything is included in that `TicketManager` but this is already a nice draft which can be improved upon.

My point here is that it is that simple. I use this approach both for boring simple and extremely complex use cases and it works wonderfully to keep things simple and connected with the domain. Just use the language that describes what is wanted from the automation, and use that in the final implementation directly.

There are multiple edge cases and scenarios to deal with within a problem domain, but the skeleton of your program is there and unless the concept of the problem changes, those won't go away soon.

This might sound obvious and dumb for some people but I stopped counting how many meetings I participated where people spoke in natural language what the program must do and in the very next moment that is lost completely by someone trying to translate to code using other terms and structure.

Our language already gives us what we want: verbs and nouns describe actions, capabilities we want to automate with the help of a computer program. Those same verbs and nouns should become function names within the software.

It is that simple, people overcomplicate things too much.

P.S.: Can't say for sure if this works nicely for other types of domains (scientific, research), but I would assume that it does. We humans create concepts using our natural language all the time. Writing software that represents those concepts is what programming is all about.