The project I'm currently on has a good deal of complexity. Its built on a 10+ year old codebase that has passed through multiple development groups with varying levels of skill and pressures. It can cause some problems around the team's ability to deliver new functionality with speed and quality.
I'm currently in the midst of working with the team to get our team on the path of making the project less complex and more maintainable over time. Here's our approach:
Schedule Regular "Debt Payments"
If all new development goes into business driven features, and the complexity and cost to adding those features continues to rise, your project grinds to halt, hurting everyone. Therefore its a good practice to bake in "debt payments" into your release planning, mean a certain percentage is reserved for improving your codebase. This time is dedicated to things like driving down static analysis issues, refactoring brittle parts, building in automation, etc.
Keep a Debt Payment Backlog
Just like the business keeps and prioritizes a feature backlog, the development should keep and prioritize its debt backlog. Just like team meets regularly to "groom" the business story backlog, the dev team should meet regularly and refine its technical backlog, so that it makes the most of its debt payments. Care should be taken with this backlog and changes to the codebase should be made responsibly and socialized with business stakeholders. They like to hear things like what is the risk involved with a change, and what tangible benefit will it give the product.
Socialize Architectural Direction and Patterns
Part of what got us into a bit of trouble in the codebase was that we inherited a some less than well-architected codebase. It had a set of patterns that were less than ideal, and when developers went in to add new functionalities, they copied how things were done for the sake of consistency and ease, propagating the patterns. Additionally different developers might have different ideas of how the same codebase should be developed. This phenomenon of propagating anti-patterns and disjointed coding will continue unless the team gets on the same page. What has helped our team is coming together regularly and talking about the various inherited parts of the system and coming up with a shared understanding of where we want that architecture to go.
Work Effectively with Legacy Code
The Boy Scout rule and Broken Windows theory are important concepts to build into your team culture. If we fix a bug or add a new piece of functionality to an existing piece of code or develop something new, we want make sure that the code has no static analysis issues, has low complexity, and is covered by tests. Michael Feather's book on the subject is a great read for coming up with ways to effectively change existing code.
Understand and Manage Pressures
What I mean by this is that the development team needs to be in charge of what they deliver. I've been on too many projects were quality was sacrificed for initial delivery speed, and this is what I have come to: If we as developers are going to sign our name to something (which we do when we commit it to source control), it needs to be done with quality and care. Its a professionalism issue and needs to be baked into a team's culture. This requires an ability to be honest with yourself and with your stakeholders, to be able to negotiate, and to be able to say no when you need to.
Getting on a path of creating clean and maintainable code can bring new purpose and life to a development team. Its great to be on a team that is proud and in control of what they do.