Some projects avoid upgrading contracts at all. They deploy a new version, create a new UI and point this new UI to new contracts. Uniswap V2 vs Uniswap V3. This way users are actively opting into new set of contracts.
It seems like for any contract that holds significant value it would be insane to make it immutable, particularly when it's written in a turing complete language like Solidity.