Refactoring Monoliths to Microservices with the BFF and Strangler Patterns - WunderGraph
Prithwish Nath
August 16, 2023·17min read
This article is archived and no longer maintained. It describes an earlier approach using WunderGraph as a Backend-for-Frontend layer to support monolith to microservices migrations. The architecture, APIs, and implementation details may not reflect current best practices or the current WunderGraph product. For current documentation and guidance, see WunderGraph Cosmo
Refactoring Monoliths to Microservices with the BFF and Strangler Patterns
The idea of having loosely-coupled services as the primitive building blocks of a system — as opposed to monolithic development — isn’t exactly a hot take. Microservices offer modularity, independent scalability, and the ability to change, fix, and adopt new things as and when needed.
The problem, of course, is the word developers universally hate — a rewrite. Monoliths can contain years worth of accumulated code. How do you even begin to untangle the patchwork of bug fixes, legacy workarounds, intricately woven dependencies, and duplicated code that probably doesn’t belong?
Full, painful, lengthy rewrites done in one go have too much potential for both financial and strategic mistakes that can sink your company (see: Netscape 6). You need something that can help you migrate from monolithic architectures to microservices in a slightly less disastrous way.
So what’s to be done? Let’s talk about the combined power of two strategies: the Backend for Frontend (BFF) and the Strangler pattern, the formidable synergy that exists between the two, and then build and migrate a full-stack app from a monolithic to a microservices-based backend using the combination.
The Strangler Pattern
[the huge strangler figs that] grow into fantastic and beautiful shapes, meanwhile strangling and killing the tree that was their host.
— Martin Fowler, The Strangler Application
Basically, you throw an intermediary interface/proxy in front of the old monolith. While it starts out as a pass-through for incoming requests to the monolith, over time this new interface would route all of the old monolith’s functionality to new microservices, as they are implemented, one at a time. It would grow around the old monolith — “strangling” the older architecture — which you can then safely decommission, having moved over entirely to the microservices.
The idea is incremental adoption. You reduce the risks inherent with a ‘Big Bang’ rewrite, retaining the possibility to deliver new features while refactoring the older monolith. You have a new system in production right off the bat — even if it’s just a proxy initially — and with incremental development you get feedback ASAP, and can course correct accordingly.
BFFs To The Rescue
Turns out, the two issues we’ve just talked about are exactly the things the Backends-for-Frontends (BFF) pattern can solve.
In the BFF pattern — first popularized by SoundCloud, and detailed by Sam Newman in this blog post — separate backend services are created for different frontend clients (or user experiences) to cater to their specific needs. Each backend service is tailor made for a specific frontend, encapsulating client-specific logic, and optimizing data aggregation, performance, and security for that specific client, while enabling independent development and scaling.
With the BFF server being the only ‘backend’ as far as your client is concerned, you hide the real backend completely from the client. Your client apps are no longer making direct calls to downstream services. It is their much faster dedicated server layer (the BFF) which is doing so to fetch, aggregate, and cache data needed by the client.
This means you’d be free to use the Strangler pattern on your monolith, extracting out functionality to the new microservice incrementally — while a BFF server is your Strangler’s proxy. No exhausting changes, rerouting, or adapters will ever be needed for your frontend.
Starting from a Monolithic API
We’ll start with a monolithic API, set up a BFF server for our NextJS client that uses it, and then migrate that monolith to microservices without a single change in our NextJS app. Our BFF will absorb the changes, shielding the client from underlying changes completely.
Step 1: The Express.js API
So this is our API, a service that provides product, customer, and order information. Let’s stick to serving static data for the purposes of this guide.
Step 2: The BFF
We’ll use WunderGraph’s CLI tool for this.
npx create-wundergraph-app my-project -E nextjs npm install
This sets up a basic NextJS 13 project (the tried-and-true pages router), TailwindCSS for styling, and a .wundergraph directory in your project root that contains config, operations you’ll define, and the unified, typesafe API we’ll eventually generate.
Step 3: The Next.JS Frontend
Our UI/UX design is dead simple. A tabbed display — one for each of the three kinds of information we’ll need.
./.wundergraph/wundergraph.config.ts
Get rid of the SpaceX API, and include this in its place. Notice the apis dependency array. Any data dependency (OpenAPI REST, GraphQL, Federations, PostgreSQL databases, etc.) you introspect with WunderGraph will need to be defined as a variable, then included in that dependency array.
Congratulations, you’ve successfully migrated your monolithic API to a microservices architecture, bypassing the drawbacks and risks of the Strangler pattern by using a Backend-for-Frontend server.
Where to go from here?
Refactoring a monolithic app to microservices requires more than just technical prowess and elbow grease. The Strangler pattern helps by providing a roadmap for incremental adoption. With its gradual and adaptive approach, it helps mitigate the inherent risks of migration by allowing seamless integration of new services while retiring legacy components organically. But there’s still so much rewiring needed.
The Backend-for-Frontends (BFF) pattern is exactly the missing piece here, allowing you to decouple the frontend from the backend in a way that’s meaningful enough to shield the former from any changes taking place in the latter — cutting down on the need for complex rerouting and adapters, making it the perfect complement to the Strangler pattern.