Skip to main content

Command Palette

Search for a command to run...

Brownfield Migration: The Strangler Fig Approach to BFF Adoption

Incrementally introducing a BFF in front of an existing monolith or REST API without a big-bang rewrite.

Published
β€’20 min read
Brownfield Migration: The Strangler Fig Approach to BFF Adoption

Every article in this series has described building a BFF on a greenfield system β€” a clean slate where the architecture is decided upfront and the frontend and BFF are developed in parallel. That is not the situation most engineers face. Most engineers face an existing frontend that talks directly to an existing API, accumulated over years, with real users depending on it every day.

The Strangler Fig pattern is the answer for this situation. Named by Martin Fowler after a species of tree that grows around an existing structure and gradually replaces it, the pattern describes a migration strategy where the new system is built incrementally alongside the old one. Traffic is moved piece by piece β€” one endpoint at a time, one screen at a time β€” until the old system is no longer needed and can be removed. At no point is there a cutover where everything changes simultaneously. At no point are users exposed to an untested replacement of the entire system.

This article covers how to apply the Strangler Fig pattern specifically to BFF adoption: how to introduce the BFF as a routing layer in front of an existing API, how to migrate endpoints incrementally, how to manage the coexistence period without duplicating logic, and how to know when the migration is complete and the old API can be retired.


The starting point: what brownfield looks like

Before the migration begins, a typical brownfield architecture looks like this:

The Vue application makes direct HTTP calls to the existing API. The existing API was not designed with the frontend's needs in mind β€” it exposes domain entities rather than screen-oriented responses, it handles authentication in a way that predates modern security practices, and its response shapes have accumulated inconsistencies over years of development.

The problems this causes are the same ones Article 1 described: overfetching, underfetching, adapter logic in the frontend, and no clean place to add cross-cutting concerns like session management or response caching. The question is how to introduce a BFF to solve these problems without rewriting the entire system or breaking the existing users while doing so.


The Strangler Fig approach, applied

The migration proceeds in four stages. Each stage is independently deployable and leaves the system in a working state. There is no stage that must be completed before users can continue using the application.

The key architectural enabler is that the BFF starts as a pass-through proxy. In Stage 1, every request from the Vue application goes to the BFF, which forwards it to the existing API without modification. No functionality changes. No user-visible behaviour changes. The BFF is in the path but does nothing yet. This is the foundation that makes every subsequent stage safe.


Stage 1: The transparent proxy

The first deployment of the BFF does not aggregate, shape, or transform anything. It proxies every request to the existing API verbatim. The purpose of this stage is to establish the BFF in the request path, validate that it can handle the traffic without introducing latency or errors, and give the team confidence in the deployment and monitoring setup before any migration work begins.

The YARP reverse proxy

.NET has a first-class reverse proxy library β€” YARP (Yet Another Reverse Proxy) β€” that makes the transparent proxy stage straightforward to implement:

dotnet add package Yarp.ReverseProxy
// Program.cs β€” Stage 1: transparent proxy
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

// Observability β€” even in proxy mode, every request should be traced
builder.Host.UseSerilog((ctx, cfg) => cfg
    .ReadFrom.Configuration(ctx.Configuration)
    .Enrich.FromLogContext()
    .Enrich.WithProperty("Service", "bff")
    .WriteTo.Console()
    .WriteTo.ApplicationInsights(
        ctx.Configuration["ApplicationInsights:ConnectionString"],
        TelemetryConverter.Traces));

builder.Services.AddApplicationInsightsTelemetry();

var app = builder.Build();

app.UseMiddleware<CorrelationIdMiddleware>();
app.UseSerilogRequestLogging();

// All traffic proxied to existing API
app.MapReverseProxy();

app.Run();

The YARP configuration in appsettings.json:

{
  "ReverseProxy": {
    "Routes": {
      "catch-all": {
        "ClusterId": "existing-api",
        "Match": {
          "Path": "{**catch-all}"
        }
      }
    },
    "Clusters": {
      "existing-api": {
        "Destinations": {
          "primary": {
            "Address": "https://api.existingplatform.no"
          }
        },
        "HealthCheck": {
          "Active": {
            "Enabled": true,
            "Interval": "00:00:30",
            "Timeout": "00:00:05",
            "Path": "/health"
          }
        }
      }
    }
  }
}

The Vue application's API base URL is changed from https://api.existingplatform.no to https://bff.existingplatform.no. Everything else stays the same. The BFF forwards every request to the existing API and returns the response unchanged.

What to validate in Stage 1

Before proceeding to Stage 2, validate three things:

Latency. The BFF adds one network hop. In Application Insights, compare the p95 latency of the same endpoints before and after the BFF was introduced. The acceptable overhead is typically under 10ms for same-region deployments. More than that indicates a deployment topology or network configuration issue that must be resolved before migration work begins β€” latency introduced by the proxy will compound with latency introduced by aggregation logic.

Error rate. The error rate for all endpoints should be identical before and after the BFF introduction. Any increase in 4xx or 5xx rates is a bug in the proxy configuration.

Authentication passthrough. If the existing API uses authentication (bearer tokens, cookies, API keys), verify that the BFF passes the credentials through correctly. YARP preserves request headers by default, but verify this with an authenticated endpoint before proceeding.

Stage 1 can run for days or weeks before Stage 2 begins. There is no urgency to migrate β€” the system is working, users are unaffected, and the team is learning how the traffic actually looks before beginning to intercept it.


Stage 2: Incremental endpoint migration

With the BFF in the path and validated as transparent, the migration begins. The approach: introduce BFF-handled routes alongside the YARP catch-all, using specificity to determine which requests the BFF handles and which it forwards.

The routing strategy

YARP and Minimal API routes coexist in the same application. The critical insight is that ASP.NET Core's routing matches the most specific route first. A BFF-handled route for GET /api/dashboard takes precedence over the YARP catch-all for {**catch-all}. Routes that have not been migrated yet continue to be proxied.

// Program.cs β€” Stage 2: BFF routes coexist with catch-all proxy

// Register BFF services
builder.Services.AddHttpClient<ExistingApiClient>(client =>
    client.BaseAddress = new Uri(builder.Configuration["ExistingApi:BaseUrl"]!));
builder.Services.AddScoped<DashboardAggregator>();

// ... other BFF registrations ...

var app = builder.Build();

// BFF middleware
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseSerilogRequestLogging();
app.UseAuthentication();
app.UseAuthorization();

// Migrated BFF endpoints β€” registered BEFORE the catch-all proxy
app.MapDashboardEndpoints();   // Handles GET /api/dashboard
app.MapCourseEndpoints();      // Handles GET /api/courses/{id}

// Catch-all: everything not yet migrated goes to the existing API
app.MapReverseProxy();

app.Run();

This routing structure means:

  • GET /api/dashboard β†’ handled by the BFF's DashboardAggregator

  • GET /api/courses/c-1 β†’ handled by the BFF's CourseAggregator

  • GET /api/users/profile β†’ forwarded to the existing API (not yet migrated)

  • POST /api/enrollments β†’ forwarded to the existing API (not yet migrated)

The Vue application makes the same requests to the same BFF base URL. Whether a given request is handled by the BFF or forwarded to the existing API is invisible to the client.

The migration wrapper: using the existing API as an upstream

During the migration, the BFF's aggregators call the existing API as their upstream. This is identical to how Article 4's aggregators call domain microservices β€” the existing API is just another upstream. An ExistingApiClient typed client replaces the individual service clients where the upstream has not yet been decomposed:

// Clients/ExistingApiClient.cs
public sealed class ExistingApiClient(
    HttpClient http,
    IHttpContextAccessor contextAccessor,
    ILogger<ExistingApiClient> logger)
{
    // Forward the caller's auth token β€” the existing API validates it
    private void AttachAuth(HttpRequestMessage request)
    {
        var authHeader = contextAccessor.HttpContext?
            .Request.Headers.Authorization.FirstOrDefault();
        if (authHeader is not null)
            request.Headers.TryAddWithoutValidation("Authorization", authHeader);
    }

    public async Task<JsonElement?> GetAsync(
        string path, CancellationToken ct = default)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, path);
        AttachAuth(request);
        try
        {
            var response = await http.SendAsync(request, ct);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadFromJsonAsync<JsonElement>(ct);
        }
        catch (HttpRequestException ex)
        {
            logger.LogWarning(ex,
                "Existing API unavailable. Path: {Path}. Status: {Status}",
                path, ex.StatusCode);
            return null;
        }
    }
}

Using JsonElement rather than a typed DTO is intentional for migration work. The existing API's response shapes are already known by the Vue application β€” the initial goal of the BFF aggregator during migration is not to redesign the shape but to compose multiple calls and introduce the architectural boundary. Shape refinement comes after the boundary is established.

A migration-phase aggregator

During migration, the dashboard aggregator composes calls to the existing API, performing the aggregation the Vue application previously performed on the client:

// Aggregators/DashboardAggregator.cs β€” migration phase
public sealed class DashboardAggregator(
    ExistingApiClient existingApi,
    ILogger<DashboardAggregator> logger)
{
    public async Task<DashboardResponse> AggregateAsync(
        string userId, CancellationToken ct = default)
    {
        var partialFailures = new List<string>();

        // These were previously four separate fetch calls in the Vue application.
        // The BFF now makes them in parallel and returns a single response.
        var profileTask      = existingApi.GetAsync($"/users/{userId}/profile", ct);
        var notificationTask = existingApi.GetAsync($"/notifications/unread?userId={userId}", ct);

        await Task.WhenAll(profileTask, notificationTask);

        var profile = profileTask.Result;
        if (profile is null)
            throw new BffAggregationException("User profile unavailable.");

        var orgId   = profile.Value.GetProperty("organisationId").GetString();
        var courses  = await existingApi.GetAsync($"/courses?orgId={orgId}", ct);
        if (courses is null) partialFailures.Add("courses");

        JsonElement? sessions = null;
        if (courses.HasValue)
        {
            var courseIds = courses.Value
                .EnumerateArray()
                .Select(c => c.GetProperty("id").GetString())
                .Where(id => id is not null)
                .Take(10)
                .ToArray();

            sessions = await existingApi.GetAsync(
                $"/sessions?courseIds={string.Join(",", courseIds)}&limit=3", ct);
            if (sessions is null) partialFailures.Add("sessions");
        }

        // Shape the response β€” this is where the BFF adds value over the raw proxy
        return new DashboardResponse(
            User: ShapeProfile(profile.Value),
            Courses: ShapeCourses(courses),
            UpcomingSessions: ShapeSessions(sessions),
            Notifications: ShapeNotifications(notificationTask.Result),
            PartialFailures: partialFailures
        );
    }

    private static UserProfileResponse ShapeProfile(JsonElement profile) => new(
        DisplayName: $"{profile.GetProperty("firstName").GetString()} " +
                     $"{profile.GetProperty("lastName").GetString()}",
        Role: TranslateRole(profile.GetProperty("roleCode").GetString()),
        AvatarUrl: profile.TryGetProperty("avatarPath", out var avatar)
            ? $"/media/avatars/{avatar.GetString()}"
            : null
    );

    // ... additional shape methods ...
}

The JsonElement approach is verbose but honest about what is happening: the BFF is parsing a response designed for a different consumer and reshaping it. Once the migration is complete and the existing API is replaced by proper upstream services, these JsonElement navigations are replaced by typed DTO deserialisations. Using JsonElement during migration makes the interim state explicit rather than hiding it behind premature typed models that may need to change.


Managing the coexistence period

The coexistence period β€” where some endpoints are handled by the BFF and others are still proxied β€” is the riskiest phase of the migration. Three practices keep it manageable.

Track migration state explicitly

Maintain a migration status document as part of the repository. It should be updated with every pull request that migrates or retires an endpoint:

<!-- docs/bff-migration-status.md -->
# BFF Migration Status

Last updated: 2025-04-10

## Migrated endpoints (handled by BFF)
| Endpoint                  | Migrated  | Notes                                  |
|---------------------------|-----------|----------------------------------------|
| GET /api/dashboard        | 2025-03-01| Aggregates profile, courses, sessions  |
| GET /api/courses/{id}     | 2025-03-15| Includes enrollment status             |
| GET /api/auth/me          | 2025-03-22| Feide session replaces JWT             |

## Proxied endpoints (still forwarded to existing API)
| Endpoint                  | Owner     | Target migration date                  |
|---------------------------|-----------|----------------------------------------|
| GET /api/users/profile    | Auth team | 2025-04-20                             |
| POST /api/enrollments     | Course team| 2025-04-27 β€” needs idempotency keys   |
| GET /api/sessions/{id}    | Schedule  | 2025-05-05                             |

## Retired endpoints (no longer in use)
| Endpoint                  | Retired   | Replacement                            |
|---------------------------|-----------|----------------------------------------|
| GET /api/home             | 2025-03-01| GET /api/dashboard                     |

This document is the migration's source of truth. It prevents the common failure mode where the migration stalls because no one can remember which endpoints have been migrated and which are still proxied.

Dual-mode testing

During the coexistence period, integration tests must cover both proxied and BFF-handled paths. A test that only hits the BFF-handled endpoints will miss regressions introduced by YARP configuration changes that affect the proxied endpoints:

// EducationPlatform.Bff.IntegrationTests/Migration/ProxyBehaviourTests.cs
public class ProxyBehaviourTests(BffWebApplicationFactory factory)
    : IClassFixture<BffWebApplicationFactory>
{
    [Fact]
    public async Task ProxiedEndpoint_ForwardsToExistingApi_WithAuthHeader()
    {
        // Arrange β€” existing API stub returns a known response
        factory.ExistingApiServer.Given(
            Request.Create()
                .WithPath("/users/profile")
                .WithHeader("Authorization", "Bearer test-token")
                .UsingGet())
            .RespondWith(
                Response.Create()
                    .WithStatusCode(200)
                    .WithBodyAsJson(new { firstName = "Ingrid", lastName = "Solberg" }));

        var client = factory.CreateClient();
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", "test-token");

        // Act β€” this endpoint is not yet migrated, should proxy
        var response = await client.GetAsync("/users/profile");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        factory.ExistingApiServer.LogEntries.Should().Contain(entry =>
            entry.RequestMessage.Path == "/users/profile");
    }

    [Fact]
    public async Task MigratedEndpoint_HandledByBff_DoesNotCallExistingApi()
    {
        // Arrange
        factory.UserClient
            .GetProfileAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(new UserProfileDto("Ingrid", "Solberg", "uninett", "TEACHER", null));
        factory.NotificationClient
            .GetUnreadCountAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(0);
        factory.CourseClient
            .GetCoursesByOrgAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns([]);

        var client = factory.CreateAuthenticatedClient();

        // Act β€” /api/dashboard is migrated; should NOT hit the existing API
        var response = await client.GetAsync("/api/dashboard");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        factory.ExistingApiServer.LogEntries.Should()
            .NotContain(entry => entry.RequestMessage.Path == "/api/dashboard");
    }
}

These tests use WireMock.NET (dotnet add package WireMock.Net) as a stub for the existing API during integration testing. WireMock logs every incoming request, which allows the test to assert that the existing API was or was not called for a given endpoint.

Feature flags for gradual traffic migration

For high-risk endpoints β€” those with complex business logic or high traffic volume β€” a feature flag allows routing a percentage of traffic to the BFF before full cutover:

// Middleware/MigrationRoutingMiddleware.cs
public sealed class MigrationRoutingMiddleware(
    RequestDelegate next,
    IConfiguration config)
{
    public async Task InvokeAsync(HttpContext ctx)
    {
        var path = ctx.Request.Path.Value ?? "";

        // Check if this path has a gradual migration percentage configured
        var migrationKey = $"Migration:Routes:{SanitisePath(path)}:BffPercentage";
        if (int.TryParse(config[migrationKey], out var bffPercent)
            && bffPercent < 100)
        {
            var roll = Random.Shared.Next(100);
            if (roll >= bffPercent)
            {
                // This request goes to the existing API
                // Mark it so YARP handles it instead of the BFF route
                ctx.Items["ForceProxy"] = true;
            }
        }

        await next(ctx);
    }

    private static string SanitisePath(string path) =>
        path.TrimStart('/').Replace('/', ':');
}

Configure the migration percentage in appsettings.json:

{
  "Migration": {
    "Routes": {
      "api:courses:detail": {
        "BffPercentage": 10
      }
    }
  }
}

Start at 10% of traffic to the BFF for a newly migrated endpoint. Monitor error rates and latency in Application Insights, split by the Source: bff vs Source: existing-api label set in the middleware. Increase the percentage incrementally β€” 10%, 25%, 50%, 100% β€” over days or weeks depending on confidence. If errors appear at any stage, rollback is a configuration change, not a code deployment.


Stage 3: Authentication boundary migration

Authentication is typically the most complex aspect of brownfield BFF migration. The existing system usually uses a pattern established years ago β€” often bearer tokens stored in localStorage, JWT validation in the frontend, and a stateless API. The BFF introduces a fundamentally different model: server-side sessions, HttpOnly cookies, and Feide OIDC.

These two authentication models cannot coexist in the same session. A user authenticated with the old model cannot seamlessly transition to the new model without a re-authentication event. The migration strategy must account for this.

The dual-auth window

During the migration, the BFF accepts both authentication models simultaneously:

// Authentication configuration during migration
builder.Services
    .AddAuthentication(options =>
    {
        // Default to cookie (new model) β€” falls back to JWT (old model)
        options.DefaultAuthenticateScheme = "cookie-or-jwt";
        options.DefaultChallengeScheme    = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, opts =>
    {
        opts.Cookie.Name     = "__bff_session";
        opts.Cookie.HttpOnly = true;
        opts.Cookie.Secure   = true;
        opts.Cookie.SameSite = SameSiteMode.Strict;
        opts.Cookie.MaxAge   = TimeSpan.FromHours(8);
    })
    .AddJwtBearer("legacy-jwt", opts =>
    {
        // Existing API's JWT configuration β€” same issuer, same audience
        opts.Authority = config["LegacyAuth:Authority"];
        opts.Audience  = config["LegacyAuth:Audience"];
        opts.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer   = true,
            ValidateAudience = true,
            ValidateLifetime = true
        };
    })
    .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, opts =>
    {
        // Feide configuration from Article 6
        opts.Authority    = config["Feide:Authority"];
        opts.ClientId     = config["Feide:ClientId"];
        opts.ClientSecret = config["Feide:ClientSecret"];
        // ...
    })
    .AddPolicyScheme("cookie-or-jwt", "Cookie or JWT", opts =>
    {
        opts.ForwardDefaultSelector = ctx =>
        {
            // If a session cookie is present β€” use cookie auth (new model)
            if (ctx.Request.Cookies.ContainsKey("__bff_session"))
                return CookieAuthenticationDefaults.AuthenticationScheme;

            // If an Authorization header is present β€” use JWT (legacy model)
            if (ctx.Request.Headers.ContainsKey("Authorization"))
                return "legacy-jwt";

            // Neither β€” challenge with Feide
            return OpenIdConnectDefaults.AuthenticationScheme;
        };
    });

With this configuration, a user with an existing JWT session continues to function. A user who re-authenticates through the BFF gets a session cookie. Both are valid during the migration window.

Forcing re-authentication

The migration window should have a defined end date. After that date, the legacy-jwt scheme is removed and only the cookie-based session is accepted. Users still holding a JWT are redirected to the Feide login page on their next request. This is a deliberate, communicated breaking change β€” not a silent failure.

Communicate it to users as a security improvement ("we've updated how authentication works to improve security") and choose a date that coincides with the natural expiry of existing JWTs. If JWTs expire after 24 hours, removing the legacy scheme after 25 hours ensures no currently-valid JWT is invalidated β€” users whose session expires naturally will authenticate through the new model on their next login.


Stage 4: Retiring the existing API connection

The migration is complete when every endpoint in the migration status document has been moved from "proxied" to "migrated." At this point, the YARP catch-all route is dead code β€” no requests reach it.

Verify this with a query in Application Insights before removing it:

// Check for any requests hitting the proxy catch-all in the last 7 days
requests
| where timestamp > ago(7d)
| where cloud_RoleName == "bff"
| where name contains "catch-all"    // YARP names proxied routes by their route id
| summarize count() by name, bin(timestamp, 1d)

If this query returns results, there are endpoints that were missed in the migration. Find them, migrate them, and run the query again.

Once the query returns no results for 7 days, the YARP catch-all can be removed:

// Program.cs β€” Stage 4: proxy removed, BFF-only
// app.MapReverseProxy(); ← removed

app.MapDashboardEndpoints();
app.MapCourseEndpoints();
app.MapAuthEndpoints();
app.MapSessionEndpoints();
app.MapEnrollmentEndpoints();

app.Run();

And the Yarp.ReverseProxy package can be removed from the project:

dotnet remove package Yarp.ReverseProxy

The removal of the proxy dependency is the signal that the Strangler Fig has completed its work. The old structure has been replaced; the BFF stands on its own.


The migration timeline in practice

The education platform BFF migration was completed over nine weeks. The timeline was driven by three factors: the number of distinct endpoint groups that needed migrating, the complexity of the authentication transition, and the team's capacity alongside their ongoing feature work.

Week 1–2:  Stage 1 β€” proxy deployed, validated, traffic baseline established
Week 3–4:  Stage 2 β€” dashboard and course list endpoints migrated
Week 5:    Stage 2 β€” authentication boundary migrated, dual-auth window opened
Week 6–7:  Stage 2 β€” remaining read endpoints migrated
Week 8:    Stage 2 β€” write endpoints migrated with idempotency keys
Week 9:    Stage 3 β€” legacy JWT support removed, dual-auth window closed
           Stage 4 β€” proxy removed, YARP dependency removed

The migration was done entirely without a feature freeze. Feature development continued on the existing API endpoints while the BFF migration progressed. The coexistence routing made this possible β€” new features added to the existing API continued to work through the proxy until those endpoints were migrated.


What goes wrong in brownfield migrations

Three failure modes appear consistently in BFF migrations that stall or regress.

The migration loses momentum after the easy endpoints. The first three or four endpoints migrated are typically the most straightforward β€” read endpoints with simple response shapes that the Vue application already knows how to consume. Then the migration reaches the endpoints with complex business logic, non-idempotent writes, or dependencies on authentication state that differs between the old and new model. These endpoints take longer. The team's capacity is consumed by feature work. The migration status document stops being updated. Six months later, 60% of endpoints are migrated and no one has the context to finish.

The fix is a defined migration end date, agreed at the start of the project, with engineering management visibility. "We will complete the migration by [date]" is a commitment that changes how the team prioritises the remaining work. An open-ended migration is a migration that will not be completed.

The proxy catches bugs that the existing API already had. When the BFF is introduced as a proxy, errors that were already present in the existing API become visible through the BFF's logs and Application Insights. The instinct is to fix them in the BFF. Resist this β€” fixing existing API bugs in the BFF creates coupling between the proxy and the API's broken behaviour, and the fixes must be re-done when the endpoint is eventually migrated to native BFF handling. Log the bugs, report them to the teams that own the existing API, and let them be fixed at the source.

The Vue application is modified during the migration. If the Vue application is updated to use the new BFF response shapes before all endpoints are migrated, the frontend code must handle two different shapes for the same data simultaneously β€” one from the BFF, one from the legacy proxy. This is the most common source of migration-phase bugs. The correct sequencing: migrate the BFF endpoint first, validate it in staging, then update the Vue application's composables to use the new shape. Never the reverse.


When the Strangler Fig is not the right approach

The Strangler Fig is the right migration strategy for most brownfield BFF adoptions, but not all.

If the existing API and the new BFF will share the same domain name and the same path namespace, the routing differentiation that YARP provides requires careful configuration to avoid conflicts. In this situation, it may be simpler to accept a brief maintenance window and do a cutover rather than managing the coexistence routing.

If the Vue application is being rewritten simultaneously β€” a common scenario in brownfield projects where technical debt has accumulated enough to justify both a frontend rewrite and a BFF introduction β€” the Strangler Fig adds complexity without benefit. A clean-slate Vue 3 application and a new BFF can be developed together against a shared API contract, with the existing frontend kept running until the new system is validated. This is a parallel-run strategy rather than a Strangler Fig, and it is the correct choice when the frontend is being replaced rather than migrated.

The Strangler Fig is for preserving the existing frontend while migrating the backend layer beneath it. When both are changing at once, a different approach is warranted.


☰ Series navigation

The Frontend's Contract: Building Backends for Frontends

Part 1 of 13

A practitioner's guide to the BFF pattern β€” from architectural rationale to production-grade implementation. Covers when BFF earns its complexity, how to design a clean client-specific API layer, and what it takes to run it reliably on Azure. Stack: Vue 3 Β· .NET Core 8+ Β· Azure.

Up next

BFF Resilience Patterns: Circuit Breakers, Retries & Timeouts with Polly

Making the BFF fault-tolerant using Polly. Handling partial upstream failures gracefully in aggregated responses.