Chapter 23 of 27 · Part 10: Refactoring to microservices

Refactoring: the strangler application

Migrate a monolith step by step: set a goal, strangle it with new services, and glue old and new together.

key idea / coordinatorservice / good pathin progress / watch outproblem / failureneutral

Should you refactor?

Migrate only when the monolith causes real business pain.

Why refactor a monolithReal painSlow deliveryBuggy releasesPoor scalabilityCheck firstIs it the monolith, or the process?Automated tests may be enough
Two ways to migrateBig bang rewriteYears of duplicate workBusiness keeps changingNo value until the endStrangler applicationGrow services around itValue early and oftenMonolith shrinks over time"The only thing a Big Bang rewrite guarantees is a Big Bang!" — Martin Fowler

Should you move, and where to start

Chapter 2 begins by challenging the move itself. Istio went back to a monolithic control plane and Segment moved back to a monolith after a small team drowned in per-service deployments. Ask what you want to solve, not whether the architecture is modern. From Practical Event-Driven Microservices Architecture, ch. 2.

Common claimWhy it may not justify the move
"We can scale horizontally"A monolith can run many copies. The real pain is usually the database.
"Teams will be autonomous"A modular monolith can give autonomy too. It is partly a mindset change.
"Features will ship faster"For a small team it can slow things down. Find the root cause of slow delivery first.
"Continuous delivery"Possible with a monolith, though harder as the team and code grow.
Strong reasons: the database can only scale vertically and has become too costly; the team has outgrown the application; the stack is deprecated or you need several technologies.
Choosing the first module to moveReporting has one dependency and tolerates delay, so it is a better first move than order management.Order managementdepends on 4 modulesShipping1 dependency, strictReporting1 dependency, lenientMove first: easy, valuable, tolerant of a short stale window.Weigh difficulty, benefit and how weak a consistency level the module can accept.

Measure progress against the reason you chose, and ask the engineers how it feels. Their feedback is the most valuable metric the book mentions.

If your reason isWatch
Team autonomyDependencies between teams, feature branch lifetime, merge requests
Continuous deliveryDeployments per period, cycle time, lead time
ScalabilityIncidents during load peaks, response time, machine tier needed

Set a goal first

Building Microservices opens its migration advice with a warning: microservices are not the goal. Name the outcome you want, such as handling more load or shipping faster, and check whether something easier would deliver it. Running a few more copies of the monolith behind a load balancer may beat a long decomposition.

Start with a goal, then chip awayName the goal, try easier fixes, move in small steps and reflect. Pick the first service by benefit and ease.1 Name the goalnot the goal itself2 Try easier wayse.g. more copies3 Chip awaysmall steps, learn4 Reflectin production, adjustWhat to split firstBenefita load bottleneck? code that changes often?Easehow tangled is it with the rest?First picks: real impact, low-hanging fruitSnap CI merged its young services back into one app, learned the domain, then split again a year later.

Then move in small steps. A big-bang rewrite guarantees mostly a big bang. Chip away, put one or two services into production and see whether you got closer to the goal. The monolith is rarely the enemy and often stays, smaller. If scaling is the problem, removing the 10 percent that is bottlenecked may be enough.

Take care not to split too early in an unfamiliar domain. The Snap CI team guessed wrong boundaries, paid for constant cross-service changes and went back to a monolith. Having an existing codebase to learn from makes decomposition easier than a microservice-first start. Newman also warns that you will not feel the real pain of microservices until you run them in production.

The strangler application

Grow new services around the monolith until it fades away.

Strangling the monolithMonolithall features+ New servicesstrangler growsExtract moremonolith shrinksAll servicesmonolith goneChange the monolith as little as possible and build infrastructure only as you need it.
Ways to shrink the monolithThree strategiesNew featuresbuilt as servicesSplit UIfront end vs back endExtractbusiness capabilitiesLaw of Holes: when you are in a hole, stop digging. Build new features outside the monolith.

Code, data or UI first?

Once you pick a service, you still have to order the work inside it. Building Microservices and the Decomposing the Database chapter weigh three choices.

Three ways to move one service outCode first leaves a shared database, data first moves the tables early, both at once is a big step.Code firstmost commonMonolithWishlistShared databaseData firsttests the hard part earlyMonolith + wishlist codeMain DBWishlist DBBoth at onceavoid: big stepMonolithWishlistMain DBWishlist DBWhatever you pick, sketch how code and data will both move before you start.A half-finished split that leaves a shared database stores up trouble.
Example: extracting a wishlist from the monolith.
OrderGoodWatch out
Code firstFast visible benefit. Shows which data the service needs.Teams stop early and keep a shared database.
Data firstFinds broken joins, integrity and transaction problems early.Little short-term benefit. Needs a monolith you can change.
Both at onceOne step.A big step with late feedback. Avoid it.

Newman's own take: if you can change the monolith and worry about performance or data consistency, split the schema first. Otherwise split the code and let that show who owns the data.

Splitting the user interfacePage-based decomposition routes pages to services. Widget-based decomposition assembles widgets into a container page.Page-basedWidget-basedRouter and common navigation/albums/ pages/artists/ pagesContainer pageAlbum detailsRecommendationsSimple web. A team can ship pages end to end.Needs an assembly layer. Widgets can talk by events.Widgets suit a mixed-up page. Pages suit a website.
The UI is easy to forget. Split it too, or the benefit stays siloed.

The user interface often lags behind the back end, because you need the services before you can see how to split the UI. Do not let it lag too far. For a website, page-based decomposition is a simple default: no fancy browser code, a link is a hand-off. Widget-based decomposition is the micro-frontend style, where a team owns a widget and its supporting service.

Safe switching tools

The strangler fig is the core idea: put a proxy in front of the monolith and send each call to the old or the new code. Building Microservices adds tools that make the switch safe. The monolith often needs no change, since it does not know it is being wrapped.

Safe ways to switch trafficA proxy with a toggle, a canary release and a parallel run.Toggle in proxyswitch back any timeProxy + toggleMonolithNew svcCanary releaseramp up on good metricsProxyfew users to newMonolithNew svcParallel runone answer is trustedProxycalls bothMonolithNew svcCompare resultsNever repeat side effects: do not send two order emails or pay an invoice twice.

New features and vertical slices

Stop growing the monolith, then cut capabilities out of it one slice at a time.

A new feature as a serviceClientsbrowsers, mobile appsAPI gatewaynew paths → service, old → monolithNew service + Monolithjoined by integration glue
Extracting a vertical sliceInbound adaptersthe API endpointsDomain logicthe business rulesOutbound adaptersdatabase accessDatabase tablesthe data it ownsCut all four layers of one capability out of the monolith together.
Splitting the domain model and databaseChallengeFixObject references across the cutrefer by id: Order.restaurantIdGod classes like Ordersplit: Order + DeliveryShared tablessplit the databaseWide changes in monolithreplicate data, keep old columns
What to extract firstSpeeds developmentBusy roadmap areamany changes aheadFixes a problemPerf, scale, reliabilitya troubled moduleEnables moreUnblocks otherseases next extraction

Integration glue

A service and the monolith must read each other's data and react to each other's changes.

Integration glue between service and monolithQueriesQuery API (REST)simple, but synchronousReplica (CQRS view)fast, more complexCommands and updatesMessages or eventstransactional messagingSagafor multi-step updates
Anti-corruption layerMonolithAnti-corruptionNew servicecall legacy APIlegacy data modeltranslated objectsclean domain modelIt translates between the two models, so the monolith's quirks never leak into the new service.
How the monolith joins the event flowMonolith publishes eventsAdd publish callshard if logic is scatteredTail the databaseno code change, table-levelMonolith subscribesEvent handlerusually easyHelper appif no broker client

Moving a module out with events

Stateless code is easy to move. Data is the hard part. The book walks through moving a reporting module in small steps, never as a big bang. From Practical Event-Driven Microservices Architecture, ch. 2.

Four steps to move the reporting moduleBuild the new service beside the monolith, connect it by a queue, backfill old data, then redirect and remove the module.1 Build besidemonolith untouched2 Add a queuemonolith publishes3 Backfillrepublish old data4 Redirectremove old moduleStep 3 doubles as validation: the reports must match the ones in the monolith.Keep both implementations in step until the old one is removed.

A durable broker makes this cheap. If the new service has a bug you reset its offset and regenerate. If reports need new fields you read from the beginning. Once order management is itself extracted, the reporting service reads the new service stream without any change.

Events as the source for both systemsA queue triggers both the monolith and the new service; each processes a subset, such as one country at a time.UpstreamMessage queuesingle source of truthMonolithall other countriesNew servicecountry = CARoll out by message content: move one country, then more, then remove the monolith module.

This differs from publishing events as a side effect: the message becomes the trigger for the process. If the monolith cannot read a queue, convert requests to messages outside it, or split the monolith flow at the first step.

A proxy shifts traffic graduallyA proxy routes requests by content, such as country, to the monolith or the new service.ClientsProxyroutes by contentMonolithother countriesNew serviceGET reports?country=USA comparison job checks both databases first. Keep a rollback: rewind the queue and reprocess.

Dependencies and living with two sources of truth

A new service often still needs data the monolith owns, and the monolith may still need data you moved. A remote call is the quick answer. Keep it temporary: if the monolith slows or crashes, the new service fails with it, and scaling the service adds load to the monolith. From Practical Event-Driven Microservices Architecture, ch. 2.

A new service that needs monolith dataA remote call couples the service to the monolith. An internal view built from product events does not.Remote call (temporary)OrderCreatedeventInventory svcneeds product infoMonolith APIcascading failure riskInternal view (sustainable)Product eventsfrom the monolithInventory svclocal product viewOwn databaseor Kafka KTable

Going the other way, the monolith often joins data from many domains in one query. Joining over HTTP means pulling and merging a lot of data in memory. The sustainable choice is to feed the monolith database from the new service events, so it becomes a denormalized view while the service is the source of truth.

Sometimes two entry points must live side by side for years. In the book the inventory logic sat in stored procedures that joined order and product data and could not be switched off. Both sides then publish and consume each other changes, and the cycle is broken with an origin header.

Two-way synchronization without loopsEach system publishes its changes; each consumer ignores events whose origin is itself.Monolithedits reportMonolith queueorigin = monolithReporting svcapplies changeReporting svcedits reportService queueorigin = serviceMonolithapplies changeThe monolith ignores the service-origin event it re-published. Collisions: the later version ortimestamp wins.
Last resort: the book calls two-way sync extremely hard to run for long. Incidents are harder to trace and new features are limited by the monolith design. Keep it near the bottom of the toolbox, and aim to retire the monolith queue when no messages flow.

Consistency and security

Extraction turns ACID transactions into sagas. Plan the order so the monolith never has to undo.

Extraction order that keeps the monolith simple1. Order svcowns compensation2. Consumer svcverify consumer3. Kitchen svcpending ticket4. Accountingauthorize cardIf Kitchen went first, the monolith would need APPROVAL_PENDING states and undo logic.
Saga with the monolith as the pivotOrder svcMonolithOrder APPROVAL_PENDINGverify consumer, ticket, cardpivot: never undonesuccessOrder APPROVEDAll compensation lives in the new, testable service.
Sharing login between monolith and servicesBrowserAPI gatewayMonolithOrder HistoryPOST /loginroute loginJSESSIONID + USERINFOGET /orders + cookieAuthorization: JWTvalidate, read userOne small change: the monolith's login handler also sets a USERINFO cookie holding a JWT.

Two worked examples

A brand-new feature built as a service, and an existing capability pulled out.

New feature: Delayed Delivery ServiceMonolithpublishes eventsReplicasorders, restaurantsTimer checkfind late ordersNotifycustomer + CRMContact info is fetched by REST when needed. Order and restaurant data are replicated from events.
Extracting Delivery Service from the monolithInterfaceDeliveryServiceRefactor callersuse the interfaceFeature toggleold code or proxyDelivery svcvia messagesOnce the new path works, delete the old code. Next candidates: Order History, Order, Consumer, Kitchen.
Notification-style integrationMonolithDelivery svcScheduleDeliveryCourierLocationUpdatedcompute courier planCourierPlanUpdatedupdate local replica

Recap

Quick check

Why extract Order Service before Kitchen Service?
Show answer

Then the monolith's step is the saga's pivot and never needs undoing, so all the compensation logic lives in the new service instead of being added to the monolith.

Your first idea for the migration is to move order management, which touches four other modules. Why might reporting be better?
Show answer

It has fewer dependencies, delivers value, and tolerates stale data, so you meet distributed-system problems at a small scale and get feedback early.