Use feature flags in all phases of the development lifecycle
Many people think that feature flags are only useful for enabling features for specific users. But they can be used for many more things.
For building features
You can use feature flags to build new features avoiding using long-lived branches.
When building a new feature, you can implement and deploy small changes behind a feature flag. Employees working on the feature can have the feature enable and see the changes. Any other user won't see the changes until the feature is shipped.
Engineers, designers and PMs can test the changes during the implementation phase, without interfering with users. This is a good strategy that reduces risks when shipping a feature and reduces the chance of having lots of merge conflicts compared to long-lived branches.
For deploying risky changes
Feature flags can be used to reduce deployment risk. For any change that may have some risk, you can put it behind a feature flag and deploy it with the flag disabled.
Once the deployment is done nothing should've changed for any user. If you are conficent, enable the flag and monitor possible errors. If you see errors, you can disable the feature flag instantly, instead of doing a time consuming rollback. So, this way, you can rollback at runtime and not doing a new deployment, which is way faster.
Another benefit of this approach for teams or companies doing lots of deployments per day is that the enabling or disabling of the feature flag can be done without blocking other deployments.
For A/B testing
Feature flags can be used for A/B testing. You can put some behavior behind a feature flag and enable it for 50% of the calls. For the other 50% the code will behave in a different way. You should measure page views, conversions, clicks,... or any other metric relevant to you. Then, after a period of time you can check those metrics and compare.
For performance optimizations
If you want to implement performance optimizations, such as changing a SQL query to run faster, but it's not obvious that is going to be better in all cases, you can implement two blocks of code: one with the production code and another with the experimental code. Then, for a small % of calls make both blocks run in parallel, and compare their performance and results. Since the experimental code is not production ready, the results from it won't be returned to the user. At this point only the results from the production code are returned.
After some time you should check if the new code returns the same results and is faster. If so, you can enable it for 100% of the calls and remove the old code.
This way of shipping performance optimizations is a lot safer since it tests with real data.
For incident mitigations
Not all features in an app scale the same way or are equally stable. If a feature is more sensitive to problems, you can put it behind a feature flag so it can be disabled in case of an incident.
For example a feature could be specially heavy on writes to the database. Pausing it temporarly will make it easier to recover from problems due to high pressure to the database.
Another example is pausing/resuming workers that consume a message queue.
For sunsetting features
Instead of just deleting a feature, you can announce that you plan to delete it. If some users complain you can put it behind a feature flag and enable it only for them.
Feature flags are not just a mechanism to enable features for specific users. They can be an essential tool in all the phases of your development lifecycle.