Controlled Rollouts with Feature Flags in a React and .NET Web App

Learn how to add feature flags to control feature rollouts.

Introduction

Rolling out new features in a web application can be risky. A new feature might have unintended bugs, or it might not be suitable for all users immediately. Feature flags provide a mechanism to control the release of new features dynamically without deploying new code. One of the biggest difficulties is when new bugs are introduced with a new feature, and it requires a rollback of the latest code changes which may have included bug fixes and other features that are working correctly. Having more granular control over specific features greatly mitigates this risk.

In this post, we’ll demonstrate how to implement feature flags in a React frontend and a .NET C# API backend using SQL Server. We’ll create a simple to-do list app where a new feature—a button to sort list items alphabetically—is controlled by both a global feature flag and user-specific settings stored in the database.

Feature Flag Implementation

Our approach consists of two layers of feature control:

  • Global Feature Flag: A pipeline variable that determines if the feature is available across the frontend. This gives us the ability to turn the entire feature on or off at once for everyone.
  • User-Specific Feature Flags: Stored in the database and retrieved via an API to determine if the feature is enabled for a specific user. This gives us the ability to turn the feature on or off for specific users and allows for a controlled rollout before giving everyone access at once.

Example: To-Do List App

We’ll walk through some key pieces of an example to-do list app to demonstrate how we use feature flags to control when new features are visible to users. In this example, we’ll include a button that allows the user to sort to-do list items in alphabetical order by name if the global feature flag and user-specific feature flag are both set.

SQL Server Schema

We define tables to store user-specific features and to-do list items.

CREATE TABLE Users (
    Id INT PRIMARY KEY IDENTITY,
    Name NVARCHAR(100) NOT NULL
);

CREATE TABLE Features (
    Id INT PRIMARY KEY IDENTITY,
    Name NVARCHAR(100) NOT NULL UNIQUE
);

CREATE TABLE UserFeatures (
    UserId INT,
    FeatureId INT,
    PRIMARY KEY (UserId, FeatureId),
    FOREIGN KEY (UserId) REFERENCES Users(Id),
    FOREIGN KEY (FeatureId) REFERENCES Features(Id)
);

CREATE TABLE TodoItems (
    Id INT PRIMARY KEY IDENTITY,
    UserId INT,
    Name NVARCHAR(255) NOT NULL,
    FOREIGN KEY (UserId) REFERENCES Users(Id)
);

.NET C# API

Model and Database Context

public class UserFeature
{
    public int UserId { get; set; }
    public int FeatureId { get; set; }
    public Feature Feature { get; set; }
}

public class TodoItem
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public string Name { get; set; }
}

public class AppDbContext : DbContext
{
    public DbSet<UserFeature> UserFeatures { get; set; }
    public DbSet<TodoItem> TodoItems { get; set; }
}

API Controller: This controller shows only the endpoint for getting the features for a given user and does not include a comprehensive set of all other endpoints necessary for a specific to manage their to-do list.

[Route("api/features")]
[ApiController]
public class FeatureController : ControllerBase
{
    private readonly AppDbContext _context;

    public FeatureController(AppDbContext context)
    {
        _context = context;
    }

    [HttpGet("{userId}")]
    public async Task<IActionResult> GetUserFeatures(int userId)
    {
        var features = await _context.UserFeatures
            .Where(uf => uf.UserId == userId)
            .Select(uf => uf.Feature.Name)
            .ToListAsync();
        
        return Ok(features);
    }
}

React Frontend

In our example, we’re fetching user features directly in the component. Typically we use Redux for storing user features since they’re used across the entire application, but for simplicity, we’re demonstrating how we’re grabbing user features from the API to then determine whether or not to display a given feature on the UI. In our example, we’re assuming the user feature for storing the sort button is called “SortButton.”

const TodoList = ({ userId }) => {
  const [todos, setTodos] = useState([]);
  const [features, setFeatures] = useState([]);
  const globalFeatureFlag = process.env.REACT_APP_FEATURE_SORT;

  useEffect(() => {
    const fetchFeatures = async () => {
      const response = await fetch(`/api/features/${userId}`);
      const data = await response.json();
      setFeatures(data);
    };

    const fetchTodos = async () => {
      const response = await fetch("/api/todos");
      const data = await response.json();
      setTodos(data);
    };

    fetchFeatures();
    fetchTodos();
  }, [userId]);

  const sortItems = () => {
    setTodos([...todos].sort((a, b) => a.name.localeCompare(b.name)));
  };

  return (
    <div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.name}</li>
        ))}
      </ul>
      {globalFeatureFlag === "true" && features.includes("SortButton") && (
        <button onClick={sortItems}>Sort Alphabetically</button>
      )}
    </div>
  );
};

Additional Considerations on our Example

The feature flags demonstrated here apply specifically to the front-end. Every application serves a different set of users, and it may be that it’s necessary to restrict functionality from the API side as well. This can easily be done on the back end by checking if the user flag is set, but we would need to add an additional concept for global flag on the web API if you want the ability to completely shut off a feature across all users.

In this implementation, a SQL query must be run against the database to enable or disable features for a given set of users. Additional API endpoints can be made and an admin React app developed in order for an admin (often someone from the customer support team) to be able to turn features on or off for individual users (or sets of users) without requiring queries to be run directly on the database. This has been extremely useful for our team since we’ve had a number of times when introducing a new feature or UI element changes have resulted in users requesting the original functionality or have required additional changes to be made before they accept the new features.

Conclusion

By implementing both a global feature flag and user-specific settings, we gain full control over feature rollouts. This approach allows us to enable features for testing, limit access to certain users, and gradually release functionality. Fetching user-specific features directly in the component simplifies the implementation while still providing dynamic control over feature availability. This method is ideal for teams looking to deploy features safely in production environments.

Further Reading

Leave a Reply

Your email address will not be published. Required fields are marked *