Feature Flags: Theory vs. Reality
bpapillon.com
bpapillon.com
This is a social problem as much as a technical one: even if you have LaunchDarkly, DataDog etc making very clear that a flag isn't used, getting a team to prioritise cleanup is difficult. Especially if their PM leaned on engineers to make the experiment "quick n dirty" and therefore hard to clean up.
At The Guardian we had a pretty direct way to fix this: experiments were associated with expiry dates, and if your team's experiments expired the build system simply wouldn't process your jobs without outside intervention. Seems harsh, but I've found with many orgs the only way to fix negative externalities in a shared codebase is a tool that says "you broke your promises, now we break your builds".
A "leaderboard" email would 100% be one that I ignore as a time-waster.
I believe I read about it in a book (perhaps Software Engineering at Google) in the context of test coverage; using a leaderboard for gamification.
In the original comment:
> Especially if their PM leaned on engineers to make the experiment "quick n dirty" and therefore hard to clean up.
The PM then just needs to say that they don't have the time to cleanup because they have shit to do, and there's folks out there who pretty much live for this, and are usually well seen by management because they're management's executioner: no matter how shit the idea and execution, they'll get it rammed in.
This system is good because it gently targets multiple stakeholders.
I know that I delete unread about 80% of the internal company emails I get because they're not actually useful, and have better uses for my time.
An email like this, assuming that my own projects are not often mentioned in them, would rapidly get ignored, I imagine.
If the goal is to have the affected teams alerted to the situation, it seems like it would be better distributed directly to the teams individually (a targeted email just to the teams involved) rather than spammed to all of the devs in the organization.
This is the limitation that breaks every development practice people come up with.
That idea of formalizing the cleanup and requiring it for deployment is very interesting. It may be possible to extend it to other contexts.
Is that your experience?
I imagine it can be a kind of self fulfilling prophecy, but even then I can't imagine people replacing everything each 4 years. And if that's not your experience, then the point is moot.
Translating from corp-speak, whoever said that probably meant: "I plan to get promoted before we need to pay this tech debt down."
Here am I, sipping on my coffee and catching up on HN while waiting for my 10+ year project to finish building.
Building what? A feature flag, of all things.
As for external pressure from shareholders to skip cleanup: it's better to not describe cleanup as a separate step, but as just another part of feature implementation. It is a fact of software development that work on a feature must be done both before and after that feature is first available in production (whether it's to ensure that all is fine with a finger on the rollback button, to set up relevant production monitoring, or to clean up any scaffolding that was required for the release), and it is possible to have features that are available in production but are not yet _done_. Finishing the work is not optional.
e.g. "Test was successful so it's rolling out to all users, minus a 0.5% holdback population for the next 2 years"
This then forces the team to maintain the two paths for the long-term, ensuring the team might get re-orged / re-prioritize their projects sometime a year later making the cleanup really hard to eventually enforce.
Man, I couldn't imagine being a user in such a situation. "Oh, I guess I'm just not getting the better functionality?" Even worse if I were a paying customer.
This is more true for larger flags that substantially change the experience and may not implement niche or edge-case functionality. Obviously you want to avoid these kinds of tests if possible but it’s not always possible.
Doesn't take long.
If it doesn't break the build (or do anything else that stops it from moving forward) there will always be external pressure to get things out "ASAP" despite in most circumstances "ASAP" isn't required. If you can't full on stop whats happening, it becomes exponentially harder to enforce anything.
The impetus is necessary, or the problem is ignored.
The devs are really just proxies for the stress a PM is inappropriately pushing, though. But they are paid to not understand this problem, so getting them on board is impossible.
But people nonetheless went: "the run failed" -> "that test failed" -> "why is this test failing?" (There was a second failure in the run that was the actual reason the build, as a whole, failed, but that was blindly missed.)
That triggered a large discussion the "compromise" of which was, to not "confuse" people, to have the security test green check itself on failure.
And so now it is truly invisible.
(I've actually turned it back to the "red-X but don't block the larger build" mode since then … but it still causes confusion. I do not know how to further help people who cannot understand the output from a build that has two failures, one of which is failing on master which is on the whole green, and one of which is only failing on your branch.)
No tool you have will solve that, whomever owns the product team time allocation needs to be onboard with the idea of cleaning up old code.
Personally I tend to resist them, for much this reason. I don't mean that I never use them and you can't find any in my code, but I resist them. They need to prove their utility to me before I add them, in much the same way I tend to make dependencies prove their worth beyond some momentary convenience before they are allowed in. There are times they leap that bar, but I think that generalized resistance has helped keep the code bases in better order than they otherwise would be. I've seen other teams who did not resist and they've developed a proliferation problem.
If your software has accumulated or is built to support numerous independent client organizations, it almost certainly has features that are not used by all users, and thereby the software has implicit feature flags embedded in the data that it is already processing. Regardless of whether those feature flags in data work well or work poorly, why in the world would you want to add a second feature-control subsystem? Because it is meta-programming, I suppose, and we all know that meta-programming just adds another level of power to everything, and your first feature-control system may be a little hard to disentangle, and you can make feature-flags work by having the meta-programming done by a select few who really know what they are doing, and it will be a worthwhile challenge, and even if it doesn't work you will learn a lot, and it will look good on your resume, and give everyone a few good laughs when they realize what they were trying to do.
I caution strongly against the proposed solution to fail CI if zombie flags are detected. CI should ONLY fail if there are changes in the branch that cause the failure. Detecting zombie flags (eg, this branch contains a flag which has been turned on and untouched for 90 days) is setting a CI time bomb. Find another way to alert developers of the zombie instead of failing good code at CI time.
1. The CI failures aren’t worth it. I accept the zombie flags.
2. Here’s a specific solution that will lead to zombie flags being removed and at a lower cost.
Gesturing to “find another way” doesn’t solve it.
If the stakeholders won’t allocate time in the roadmap to remove old feature flags then you shouldn’t use feature flags.
- local/static access: it should not have to call out to a 3rd party server to get basic runtime config
- unused-flag detection: flags should have three reported states: never used, recently used, not recently used. These will be different from the user-controlled states of active, inactive, etc.
- sticky a/b testing: should follow the logged in user until the flag is removed
- integration with logger: I should be able to use it with my logger out of the box to report only relevant feature flags. Alternatively can provide a packed value of all relevant flags, would probably have to do flag state versioning.
- integration with linter: should warn me if flag has not recently been used or I used a flag in the code that is not in our database (alternatively, will upsert the flag automatically if it doesn’t exist)
- hashed flag names on frontend build: prevent the leakage of information, not a perfect solution, but I would want to avoid writing “top-secret-feature” where we can.
I fully acknowledge that a lot of solutions come close, but I haven’t looked at the current state of things in the last few years so it may have improved.
This is what drove me to build an open source solution: https://featurevisor.com/
Hadn't thought of the frontend build hashing idea - like that a lot
What brought me into the talks was that the feature flag problem is a similar scope to the central one faced by CCSs (component content systems). By definition, CCS requires the content equivalent of feature flags, implemented in a variety of ways, depending on . . lots of things. That problem is this: both transclusion and conditionals necessarily couples the content to the business or product architecture. Ergo, when the product architecture goes bananas, so does your content system, and you find yourself with documents that aren't meaningful in a linguistic sense, or which just break the processor. This occurs in the content context because the natural language of a unified document is replaced in a CCS with the product or business architecture; how is a document chunked, what business needs do the conditions satisfy, at what support level are document deliverables composed. In a code context, the constructed syntax of the programming language is getting chopped by the conditionals driven from the business side; there's even more variance here regarding how code interacts with business.
So not the same problem, but the same class of problem: regular rules that have to integrate with non-regular, non-linguistic business rules.
I have a tiny chip on my shoulder regarding CCS systems, because I have seen so many years flushed down the "re-use craze" by businesses that had zero business trying to re-use anything. Feature flags are somewhat in the same bucket - a lot of things that a business wants to use flags for should really, really, really be built into the code or abstracted away - but of course a programming language has far richer ways to deal with bad abstractions than a markup language does. Which of course can be a double edged sword.
Feature flagging itself is not that hard, it's all the surrounding features that are hard. I can whip up a database engine in a couple of days, but to make it reliable, full of features, and whatever else, will take years.
That said, I'm surprised this article doesn't mention the two words that always come to my mind when I see toggles: combinatorial explosion. Several times I've worked on projects that went way too toggle-happy and decided that new functionality should be split into indefinite life "features". Just in case the company someday wants to sell a model without that feature. Of course, when an old toggle finally gets turned off a year later, you realize that it crashes the system because several other features kind of half depend on them.
Yeah, once you do that you have settings, and not feature flags anymore.
Adjustable settings do come with a high risk of combinatorial explosion. Ideally, you separate the system functionality to control this problem, but that's not always possible.
Additionaly, we've got a prototype static analysis tool to finding calls to our feature flag clients in both Go and Rust too.
when we've built our own homegrown system, it's opaque and often neglected. when we've used feature flag tools, we co-opt them to do things they're not meant to support (e.g. persistent toggles in admin panels) so end up with complexity in the code and in operational processes around it.
agree wholeheartedly with points in this article ... there are issues with how we manage flags generally, but we also bias towards assuming that once a feature is live, we can and should move on -- the feature is now persistent, part of a package, and it won't change frequently or ever.
the reality is the feature lifecycle takes on a very different shape, and, at least in my experience, current FM tooling isn't built to accommodate that.
There are a few benefits: removing features is dead simple, just delete the whole feature module, and there's no conditional branching in the base application.
There are some drawbacks too: the base application must have entry points for the feature modules to overwrite. Usually the default values are no-op or some default behavior. Features also must implement setup and teardown, which can take longer to write than a conditional.
- When adding a flag immediately file a bug to remove the flag by a certain date. Enforce in code review. The bug count will surface the problem to the management.
- When a flag is past due date start firing non-fatal incidents. The incident count will also surface the problem to the management.
It can feel good to make people do their chores, but you can also burn a _lot_ of bridges by forcing relatively minor maintenance work to be high priority like this.
Raising awareness to management is great though. Especially if you can quantify it in terms of things that matter to them more than "tech debt". Like startup latency or a direct dollar cost.
I traced the problem back to the fact that we had 100+ feature flags that were fully launched, but still loaded into the backend when "all feature flags" were loaded for a team. The way this was implemented returned all team IDs that had the feature flag, and the way this was done had some flags with multiple thousand IDs in them.
So 100+ flags, many with 2000+ int entries.
We ended up quickly shipping some code to mark features GA, so they wouldn't be loaded from Redis. Cut usage by 99% instantly.
What happens when your homegrown feature flag microservice (because why pay for a hard cost when you can have the soft cost of making your own) goes down, even temporarily.
Sane defaults at code review time, before launch aren't always the sane defaults after a feature has fully launched, or nearly fully launched.
I've seen more than a few egregious outages due to a feature flagging tool being down and taking the user experience back a year or two.
Would love to hear if others have had success with automated flag cleanup.
Removing them came down to team discipline.
Ideally, a Google like clang analysis of code would find flags ready for removal and alter code to remove the old code path. Recall Google used tools like this ro update or migrate deprecated api calls
Bbg however never got there. Instead you'd just get various alerts
My experience with LaunchDarkly has been that a lot of these hygiene-related exist only in their top tier enterprise plans, and even below that point the cost of the tool starts to draw attention.
On the product UX side - I've found these tools are designed for engineering/Devops users but (whether by design or not) by product, success, and some ops users as well.
One of the reasons I went for an open source solution ( https://featurevisor.com ) that's Git based, and every change is done via Pull Requests.
Building blocks:
- Attributes for conditions: https://featurevisor.com/docs/attributes/
- Segments for targeting users: https://featurevisor.com/docs/segments/
- Features with variations and rules: https://featurevisor.com/docs/features/
Process:
- Merge PRs
- Trigger CI/CD pipeline: https://featurevisor.com/docs/deployment/
- Consume with SDK: https://featurevisor.com/docs/sdks/
Use cases:
- User entitlements: https://featurevisor.com/docs/use-cases/entitlements/
- Testing in production: https://featurevisor.com/docs/use-cases/testing-in-productio...
- A/B testing & experimentation: https://featurevisor.com/docs/use-cases/experiments/
- Remote configuration: https://featurevisor.com/docs/use-cases/remote-configuration...
You can also generate types as a package for compile-time safety:
- Code generation: https://featurevisor.com/docs/code-generation/
The post and the comments here give me more ideas on how to improve it with more features now.