Your solution that you warn against (because of simplification) is in fact, in simple terms, the only solution. The problem you face is the same problem of coders since time (of coding) began. A better way to understand your solution, and therefore your problem, is just to describe in a different way. Let me explain…
You want two (or more) units of code to reference each other. While it is not impossible for that to happen (double-linked lists do this, for example) what you are trying to force is where the code execution dives deeper into itself at each iteration (in your case, a user’s touch on the interface) and this will eventually cause an infinite loop, stack overflow and, finally, memory exceptions (if the stack overflow doesn’t kill it first.)
The solution is, as you’ve already mentioned, to use listeners. The confusion comes by how to set them up, not by their linkages.
So, and this will be somewhat general because you’ve not provided any code, lets say that module A, B, C and D all refer to each other. That’s impossible. But it can be done. Fortunately, module A is - in your case - the main.lua code, so we’re safe here.
Modules B, C and D all get loaded by module A with require statements. One approach is, in module A, to call a function which let’s you register the other modules with each module. So, first load them all, then tell each one about the others. This works, but it’s not elegant because you can still end up creating infinite loops, stack overflows and the like.
The better solution is, in simple terms, to have each module listen to some object in the system, let’s say Runtime, for some event. You can also use scene objects or any display object, like display groups, for example.
So, if object B needs to listen for events fired for object C, and D listens for events fired by objects A and B, they can. They are not attaching function calls to another object; They are simply listening for events being fired - it doesn’t matter what they are being fired by. This is a change in perception by how the system is setup, not by the relationships between objects. Part of this is “de-coupling.”
Now, take this a step further and look at the system objects or demo samples in the Corona folder. You can see that the widget library has objects which receive touch events, for example. They don’t care what needs to be told that the touch event occurred and that some function somewhere needs to be called - they simply provide a mechanism to let another object attach a callback function or fire an event themselves and let whatever else in the system listen for that event.
The key to understanding the architectural problem here is to think, “What if each thing in my application did not rely on anything else? What if every single user interface object and game/app object existed in a world of it’s own and did not - must not - rely on any other object?”
Start from that point and you’ll realise two major things: First, you’ve probably got a lot of re-writing/structuring to do and, second, that you have just realised how to simplify the architecture and make a lot of your code re-usable (because it won’t be innately tied to other pieces of code.)
Hit me back if you need anything clarifying, but before you do I highly recommend that you post some boiled down sample code - that in itself (boiling) might lead to some useful realisations, too.