I find Event Driven Architecture and Command-Query Responsibility Segregation to be the most common techniques I use to conquer complexity.
In a nutshell:
- UI Controllers submit granular Commands on behalf of the user
- Command Handlers mutate application state through subsystems (like a domain model, or simply transaction scripts)
- Changes in application state raise events
- Event handlers react by submitting more commands and/or interacting with application services (updating auxiliary data for display, sending emails, etc., etc. - a lot happens here and this is the main method of decoupling auxiliary logic from that logic that modifies the application state)
On a large scale, I try to stick somewhat rigidly to the send a command, handle the command, raise events, handle events pattern - it can lend large scale organization to a variety of project types.
Then, I allow handlers to achieve their function through whatever mechanism seems appropriate. These mechanisms form sub-components of the application like a domain model with persistence, loggers, email helpers, etc.
Allowing flexibility in the implementation of these sub-components enables agility (write it to get it done, if need be), code reuse (whether linked library or copy and paste), refactoring (let's base off of this previously written component but improve/change it as so).
But sticking to EDA & CQRS gives us some architectural consistency across projects, which makes navigating a foreign code base much easier. It also provides nice points to implement functionality with AOP - like authorizing & recording commands, persisting events, distributing workload, etc.