How we manage Feature Flags šŸš€

Felipe Ɓlvarez
Mercadona Tech
Published in
4 min readJul 21, 2023

Intro

Hi everyone! The purpose of this post is to provide a brief explanation of how we typically manage feature flags on our team. This includes adding them to code and tests, as well as the most important aspect: deleting them in the easiest way possible to avoid errors

We are really lazy programmers that donā€™t like thinking too much, and therefore we always try to simplify our work in a way that helps us to focus on the important things and avoid losing our time and energy on minor details

I will demonstrate this with an example of simplified code (in the real world, we make some intermediate steps due to the use of TDD) šŸ’»

The problem

Letā€™s put a little bit of context. Think about we have to make a change inside one of our use cases. As usually, we want to add a feature flag that controls that change (we use FFs to everything, i mean EVERYTHING)

Okay, so we want to change, for example, a filter in one of our actions that only retrieves some info of our database, how we start?

The action

We have our action like this:

class RetrieveStuff:
def execute(self, name: str) -> Stuff:
return Stuff.objects.filter(name=name).first()

First we refactor itā€™s name, adding the suffix Old, and then we add our new action:

class RetrieveStuffOld:
def execute(self, name: str) -> Stuff:
return Stuff.objects.filter(name=name).first()

class RetrieveStuff:
def execute(self, name: str) -> Stuff:
return Stuff.objects.filter(name=name, is_active=True).first()

The action tests

Here we follow the same approach that we have follow with the action. First we have our tests (now with the refactored name that we did before):

class TestRetrieveStuffOld:
def test_retrieve_stuff_by_name(self):
Stuff.objects.create(name="a_name")

stuff: Stuff = RetrieveStuffOld().execute("a_name")

assert stuff.name == "a_name"

And then, we duplicate the tests for the new action and make the changes we need:

class TestRetrieveStuffOld:
def test_retrieve_stuff_by_name(self):
Stuff.objects.create(name="a_name", is_active=False)

stuff: Stuff = RetrieveStuffOld().execute("a_name")

assert stuff.name == "a_name"


class TestRetrieveStuff:

def test_retrieve_stuff_by_name_and_active(self):
Stuff.objects.create(name="a_name", is_active=True)

stuff: Stuff = RetrieveStuff().execute("a_name")

assert stuff.name == "a_name"
assert stuff.is_active

Adding it

Okay, so now, we have our new action prepared and tested, but we are not using it, because our view (our infra enter point to the actions) should have this look:

class RetrieveStuffView(APIView):
def get(self, name: str):
stuff: Stuff = RetrieveStuffOld().execute(name)

return Response(data=StuffSerializer(stuff).data)

What we usually do at this point is making a PR and deploy to PROD šŸš€

Then, itā€™s time to add the FF into our view:

class RetrieveStuffView(APIView):
def get(self, name: str):
if not flags.is_active("use-new-filter-for-stuff"):
stuff: Stuff = RetrieveStuffOld().execute(name)

return Response(data=StuffSerializer(stuff).data)

stuff: Stuff = RetrieveStuff().execute(name)

return Response(data=StuffSerializer(stuff).data)

Note that we add it with a not statement, this will help us to not thinking later when we have to delete it. We will explain this later

The view tests

Here is much more the same as the action tests. We have our view tests, and then we duplicate them, adding a fixture to enable or disable the flag for each test class:

class TestRetrieveStuffViewWhenFlagDisabled:

@pytest.fixture(autouse=True)
def manage_flag(self, activate_flag):
activate_flag("use-new-filter-for-stuff", False)

def test_retrieve_stuff_by_name(self, client):
response = client.get("/api/stuff/a_name/")

assert response.status_code == 200
assert response.json() == {
"name": "a_name",
"is_active": False,
}



class TestRetrieveStuffView:

@pytest.fixture(autouse=True)
def manage_flag(self, activate_flag):
activate_flag("use-new-filter-for-stuff", True)

def test_retrieve_stuff_by_name_and_active(self, client):
response = client.get("/api/stuff/a_name/")

assert response.status_code == 200
assert response.json() == {
"name": "a_name",
"is_active": True,
}

Nice! Now we can make another PR with this code, create and activate the FF in our systems and start using the new functionality in our production environments šŸ¤˜

Deleting it

This is, for me, the better part. We usually do this after 2 days of having deployed the new code in PROD (or after we think the new code is 100% validated)

Thanks to the fact that everything has been duplicated, we can now remove this FF and the old code without hesitation. Itā€™s as simple as:

The view
The action
The view tests
The action tests

And thatā€™s it! We just need to remove everything that have the suffix Old or WhenFlagDisabled, without thinking about ifs, tests, or complicated paths

Conclusions

Maybe you can think that this approach of development produces a lot of duplicated and redundant code (and you are right šŸ™„), but itā€™s for a really short period of time. We think that the advantages that this approach give us:

  • An easier development
  • Have our 2 paths perfectly separated, including the tests
  • A really easy and fast process of removing the old code
  • Reduce the human errors inserting the new code or testing it

Are very worthwhile compared to insert the flags inside and mixed with our production code. I encourage you to try it, believe me, no one will charge you for uploading more code lines to your repo šŸ˜‰

--

--