If you've ever worked on a large or mature code base, you've almost certainly encountered the phenomenon that parts of it are just awful. These are the parts everyone hates working on. The parts that are delicate and brittle. Where even minor changes turn into major efforts because every little thing creates more problems than it solves. If you made a map of your code base, this part would be labeled "here be dragons". And if you've ever had to explain this reality to some non-developer stakeholder, they probably wanted it to be fixed. Your team probably begged for permission to fix it. Everyone was on the same page, you all wanted the same thing, and then nothing happened. Why? Because when they were asked what the solution was, the team said refactoring.
That particular understanding of refactoring is a terrible idea, and the product owner was probably right to say no to it. When developers talk about refactoring, it's typically as a big one-time effort undertaken to pay down a chunk of technical debt. This may or may not be successful, but in either case it doesn't address the reason that technical debt was accumulated in the first place. Which means that it will re-occur, and then you're eventually back in the same place you started: hunting dragons and begging for a big "one-time" investment to fix things.
You may now be wondering: how do we fix these things? The answer is of course by refactoring; but not as it's own task. In order to be successful, refactoring can't be some project or event or special effort. Because it doesn't end. You should never stop refactoring. And that means that it needs to be a part of your regular work flow. To succeed at refactoring, you need to make small, incremental improvements. It needs to be continuous. It needs to be habitual. And it needs to be proactive. As a practical bonus, when you view refactoring this way, it ceases to be something you need to get special permission for. Because it's not special; it's just a regular part of development. This is continuous refactoring.
How to do that
This is the most important thing: working this way is a team decision. You have a build a culture that supports refactoring. You have a mess that you're trying to clean up. But other people helped make this mess, and they're probably still making more messes. The first and most important step in enabling continuous refactoring is to agree as a team that you're going to do it, and what the goals are. What is the perfect ideal form of your code that you're all working toward? Everyone needs to have the same understanding of what that goal is. This also means you have to have a common understanding of what the problems are.
So the first part is hard. I don't know if I can help you with it. It's going to involve talking to your fellow developers, and your managers, and your project managers, and possibly a lot of other people. You have to convince them that making small improvements as the opportunity arises will be good for the project overall, even when they're not strictly necessary to complete a task. If you're lucky, they already feel the same way. If not, I don't know. Maybe you can share this article with them. In all likelihood, any argument against continuous refactoring is that it creates more change than is necessary and change creates risk and increases costs. That's a reasonable point of view, but it ignores the reality that these problem areas are already sources of increased risk and cost. I've written before about how you can mitigate that risk. Those methods are completely compatible with continuous refactoring.
Once they're on board with the basic premise of continuous refactoring, you need to have more conversations about what does and doesn't need to be refactored. You need to agree on code styles. You need to agree on design patterns. You need to share and communicate and remind each other of changes you've already made so they don't get duplicated or ignored.
The second part is easier. There are problems. You know it. Your team knows it. You mostly just have to find them. The best time to start looking is during code review. If you weren't already, be sure to ask why any given change was necessary. If it's not obvious, recommend adding documentation about the purpose of any given block of code. And build the habit of looking at the things that didn't change, and ask why. This other function works, sure. But is it clear? Is it SOLID? Is it DRY? Does it meet your team's new standards? To keep all these refactors from becoming a distraction from the changes you made for the task you're working on, it's best to do things in two stages. First, do the refactor. Second, make the changes that will alter your app's behavior. These can then be reviewed separately, and you'll probably find that this makes the review easier and faster rather than taking additional time. The refactoring changes just need to be reviewed for consistency with the previous behavior, and compliance with your team's standards. There's no new feature or bug fix to review. Then afterward for the new feature or bug fix, the code starts in a state that's easier to read and understand. The style and conventions are familiar. You only need to spend mental energy on understanding the new logic and not on parsing the code.
In the context of refactoring, unit tests serve two important purposes. First, they give you confidence that refactors really are purely refactoring and haven't accidentally introduced new bugs. You should have tests when you begin, and they should pass. You should still have tests at the end, and they should still pass. And if they were well designed, they should have needed little to no changes themselves to accommodate the refactor. Second, they point toward the parts of your code base that are the most in need of refactoring. If there are parts that are not tested at all, it's very likely because they're hard to test. And if some code is hard to test, it almost certainly has other problems. At a minimum it's probably hard to understand and maintain. The same goes for unit tests that are slow, or unreliable, or overly large. These things all suggest problems which should be addressed during a refactor.
In case you're not familiar, linting is a form of static analysis that checks source code for patterns that are likely to cause problems. One of the checks you can do is for style rules. Building an automated linting step into your workflow will make development faster and code review easier. Linting is very fast and can generally be done in real time. So developers can get feedback on their design immediately. If it's too complicated or too long or just has a silly oversight they can fix it before it ever goes to review. And once it's in review, the reviewers can be confident that it matches the team style guide and can focus on more important things, like logical correctness. Plus as a reviewer, it's nice when you don't have to make comments about style. Style is important, but it feels nit picky and code review feels more important when our machines do the nit picking for us.
If your team is really good about this, you may eventually run out of obvious trouble spots that need refactoring. Or, if you're starting from a really awful beginning then there may be so many problems that you don't know where to start. In both cases, having some quality metrics can be helpful as a guide. Test coverage is the first metric to try out. Anywhere that test coverage is low, you should add tests. If adding tests is hard, that's something that needs to be fixed. Dependency analysis is also informative and easy to do. Run your app through some dependency analysis and see what you get. Modules with many dependencies are good candidates for refactoring because it's probable they have too many responsibilities. Modules with only one dependency—or only one dependent for that matter—might be duplicating something else and can be removed. And speaking of duplicates, other static analysis tools can find duplicated or mostly duplicated code for you. These are obviously prime candidates for refactoring by replacing them with a single reusable implementation. If you're feeling adventurous, then you can even mine your source control history or your issue tracker for areas of code which have historically caused problems.
I'm done; you're not
So, there are a lot of tools to help with actually performing refactoring, and for finding the code that needs it most. There are techniques to use when actually doing the refactor. But none of that matters if your team doesn't actually do it. So go talk to them, convince them that continuous refactoring will be helpful. Agree as a team to actually do it. Hold each other to that standard. You've got this.