Skip to Content
Course content

229: Practice Exercise: Building a Type-Safe Feature Flag System

Click on the "Edit" button in the top corner of the screen to edit your slide content.

In a real-world production environment, you almost never want to deploy a major feature to 100% of your users the second the code hits the server. We use feature flags to decouple deployment from release. But if you've ever worked on a large codebase, you know the horror of "stringly-typed" flags—where a tiny typo in a configuration key like "new_checkout_flow" vs "new_checkout_flwo" results in a feature being silently disabled for everyone.

I want to show you how to build a system that makes those typos impossible by leveraging Scala's type system. We'll build a simple manager for an e-commerce site that handles a new AI-powered search and a promotional banner.

The danger of string-based keys

When I first started implementing flags, I did what most people do: I used a Map with String keys. It feels flexible, but it's a trap. Let's look at how I'd initially approach this, and why it fails.

case class User(id: String, isBetaTester: Boolean)

class FeatureFlagManager(flags: Map[String, Boolean]) {
  def isEnabled(flag: String): Boolean = flags.getOrElse(flag, false)
}

val config = Map("ai_search" -> true, "promo_banner" -> false)
val manager = FeatureFlagManager(config)

// Everything looks fine until...
val status = manager.isEnabled("ai_serch") // Typo! Returns false.

The compiler has no idea that "ai_serch" isn't a real flag. In a large app with hundreds of flags, this leads to bugs that are incredibly tedious to track down because the code is technically "correct" (it's just returning a default value).

Letting the compiler catch our typos

To fix this, we need to move the definition of our flags from the configuration layer into the type layer. I prefer using a sealed trait or an enum for this. This way, the isEnabled method doesn't take a String; it takes a specific type that the compiler can verify.

sealed trait FeatureFlag
case object AiSearch extends FeatureFlag
case object PromoBanner extends FeatureFlag
case object NewCheckout extends FeatureFlag

class TypeSafeFeatureManager(flags: Map[FeatureFlag, Boolean]) {
  def isEnabled(flag: FeatureFlag): Boolean = flags.getOrElse(flag, false)
}

val config = Map(
  AiSearch -> true, 
  PromoBanner -> false
)
val manager = new TypeSafeFeatureManager(config)

// Now, this won't even compile if I mistype the object name:
// manager.isEnabled(AiSerch) // Compile error!
val status = manager.isEnabled(AiSearch) // Safe and verified

By doing this, we've shifted the failure from runtime (where a user doesn't see a feature) to compile-time (where the developer sees a red squiggly line). It's a small change that saves hours of debugging.

Adding user-specific overrides

Most of the time, you don't just want a global on/off switch. You want "Canary" releases—turning a feature on only for beta testers or internal employees. I like to handle this by passing a context object into the check. I'll modify the manager to handle a hierarchy: first check the user's status, then fall back to the global config.

case class User(id: String, isBetaTester: Boolean)

class AdvancedFeatureManager(globalFlags: Map[FeatureFlag, Boolean]) {
  def isEnabled(flag: FeatureFlag, user: User): Boolean = {
    flag match {
      case AiSearch => 
        // AI Search is open to all beta testers, regardless of global flag
        if (user.isBetaTester) true else globalFlags.getOrElse(flag, false)
      
      case _ => 
        globalFlags.getOrElse(flag, false)
    }
  }
}

val manager = new AdvancedFeatureManager(Map(AiSearch -> false))
val betaUser = User("123", isBetaTester = true)
val regularUser = User("456", isBetaTester = false)

println(manager.isEnabled(AiSearch, betaUser))    // true
println(manager.isEnabled(AiSearch, regularUser)) // false

I've used a pattern match here because different flags often have different rollout logic. Some are binary, some are percentage-based, and some are user-group based. Putting that logic inside the manager keeps the rest of your business logic clean—your services just ask "is this enabled for this user?" and don't care about the underlying rules.




📋 Practical Task

Exercise: Implementing a Tiered Access Flag System

You are tasked with extending the AdvancedFeatureManager to support a "Premium" user tier. Currently, we only have isBetaTester.

Requirements:

  • Add a isPremium: Boolean field to the User case class.
  • Add a new FeatureFlag called PremiumAnalytics.
  • Update the isEnabled logic so that PremiumAnalytics is only available if user.isPremium is true, regardless of what the global configuration says.
  • Ensure that AiSearch remains available to both Beta Testers and whoever the global flag allows.

Write the complete implementation including the FeatureFlag trait, the User class, and the AdvancedFeatureManager. Test it by creating one Premium user and one Regular user, then printing the result of isEnabled(PremiumAnalytics, user) for both.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.