Minimizing Risk in Code Refactoring with Feature Flags in Vertical Slice
Refactoring critical business logic while maintaining system stability is a common challenge. This article explores how to minimize risks during refactoring using feature flags and the Alternative Handler pattern in a vertical slice architecture.
The Reality of Software Development
In the real world of software development, we often face a common scenario: The initial implementation of a feature might not be perfect. Under pressure to deliver quickly, we sometimes write code that works but isn’t as elegant or efficient as we’d like. Once the feature is live and proving its business value, we face a new challenge: How do we improve this code without risking the stability of a working system?
The Fear of Change
After a feature has been running in production and handling real user traffic, developers often become hesitant to modify it. This reluctance stems from valid concerns:
- What if the refactored code introduces new bugs?
- How can we ensure we’re actually improving the system?
- What’s the safest way to roll out changes?
- How can we quickly rollback if something goes wrong?
The Solution: Gradual Evolution with Feature Flags
Instead of replacing the entire implementation at once, we can use a “Strangler Fig Pattern” approach combined with feature flags. This strategy allows us to:
- Keep the original implementation running
- Deploy the improved version alongside it
- Gradually shift traffic to the new implementation
- Compare performance with real-world data
- Easily rollback if issues arise
Implementation Strategy
- Start with a small percentage (10–50%) of traffic routing to the new implementation
- Monitor performance, errors, and business metrics
- Gradually increase the percentage as confidence grows
- Continue until the new implementation handles 100% of traffic
- Remove the old implementation once the migration is complete
In most applications, the core business logic resides within our handlers. These handlers often start simple but grow complex over time, making refactoring risky. Instead of modifying existing code, we can introduce a new handler version and gradually transition traffic to it.
Add Feature Flag
Install package Microsoft.FeatureManagement
builder.Services.AddFeatureManagement()
.AddFeatureFilter<PercentageFilter>();
"FeatureManagement": {
"CreateOrder": {
"EnabledFor": [
{
"Name": "Microsoft.Percentage",
"Parameters": {
"Value": 50
}
}
]
}
}
Create an attribute that will decorate Command/Query with the alternative handler
Add an attribute above the command to define what is the alternative implementation that wants to replace
Add our new Handler
We create a new folder in CreateOrder to feature new V2 and contain the new handler implementation, we need the same command and the same validator and response
Create a new interface that implements the IRequestHandler
Create a new Handle by implementing this interface
We need to implement a decorator class that all requests will go through and through this class can switch between handlers like this
Now we need to update the registration of the mediator to add the decorator we don’t use the default registration of the mediator
services.AddScoped<IMediator, Mediator>();
services.Scan(scan =>
{
scan.FromAssembliesOf(typeof(CreateOrderCommand))
.RegisterMediatorHandler(typeof(IRequestHandler<>))
.RegisterMediatorHandler(typeof(IRequestHandler<,>))
.RegisterHandlers(typeof(INotificationHandler<>));
});
services.Decorate(typeof(IRequestHandler<,>), typeof(AlternativeHandlerDecorator<,>));
public static class ImplementationTypeSelectorExtensions
{
private static readonly HashSet<Type> _decorators;
static ImplementationTypeSelectorExtensions()
{
_decorators = new HashSet<Type>(new[]
{
typeof(AlternativeHandlerDecorator<,>)
});
}
public static IImplementationTypeSelector RegisterHandlers(this IImplementationTypeSelector selector, Type type)
{
return selector.AddClasses(c =>
c.AssignableTo(type)
.Where(t => !_decorators.Contains(t))
)
.UsingRegistrationStrategy(RegistrationStrategy.Append)
.AsImplementedInterfaces()
.WithScopedLifetime();
}
public static IImplementationTypeSelector RegisterMediatorHandler(this IImplementationTypeSelector selector,Type type)
{
return selector.AddClasses(classes => classes
.AssignableTo(type)
.Where(type =>
!_decorators.Contains(type) &&
!type.GetInterfaces()
.Any(i => i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IAlternativeHandler<,>))))
.UsingRegistrationStrategy(RegistrationStrategy.Append)
.AsImplementedInterfaces()
.WithScopedLifetime();
}
}
I am using Serilog and seq to test our code and see the difference i added correlation Id to my serilog to get the request life cycle now we notice that requests go to the old version first
when hit again it will go to the next handler
All source code with this in this repo GitHub also we will find the implementation of serilog and seq, and how to add middleware to add correlation ID to track all request.