Module 7: Measuring Migration Progress

The legacy-call burn-down

Overview

You already decided to measure the result (lesson 2). Now you build with it the module's central instrument: the burn-down of the calls to the legacy. A burn-down is, simply, a graph that goes downward —from the amount of work remaining to zero—, and it's the most satisfying migration metric that exists, because it's the visual evidence that the legacy is dying. Sprint by sprint, you count how many calls to the legacy monolith remain (legacy_calls), and you watch them drop: 1000, 820, 610, 400, 210, 60, 0. When it reaches zero, the legacy serves no one, and the migration of that slice reached its goal.

But the burn-down has a subtlety that separates whoever reads it well from whoever reads it badly, and it's the heart of this lesson: what matters is the trend, not the absolute value of a sprint. A sprint that reports legacy_calls = 400 doesn't tell you, by itself, whether you're going to finish. Is 400 good or bad? It depends on where you came from and where you're going. If last sprint you were at 610 and this one at 400, you burned 210 calls —you're going fast, you'll finish soon—. If last sprint you were at 410 and this one at 400, you burned 10 —you got stuck, this doesn't converge—. The same number, 400, means opposite things depending on the slope. That's why the burn-down is read like a movie (the trend between sprints), not like a snapshot (today's value), and that's why from it you can project the closing sprint.

Connection with the module. This lesson builds the progress metric that lesson 1 showed in miniature and that lesson 2 justified as a result metric. It's the strangler's traffic_percent (M3) seen over time, turned into a descending curve with velocity and projection. Lesson 4 adds the fitness function that watches that the legacy doesn't grow while the burn-down drops; lesson 6 will use the legacy_calls == 0 at the end of the burn-down as one of the done conditions; and lesson 7 will show what happens when the burn-down gets stuck near zero (the eternal migration). Notice the boundary: the traffic_percent of an individual request is decided by the M3 strangler router; here we aggregate it over time and read it as a trend. And projecting an end date isn't project planning in the abstract —it's extrapolating the slope of a concrete result metric—.

An analogy: paying off a debt and seeing the payoff date

Imagine you owe 10,000 pesos on a card and you decide to pay it off. Each month you make a payment and the balance drops. The question that really matters to you isn't "how much do I owe today?" —that's a snapshot— but "when am I going to finish paying?" —that's the trend—. And to answer it, today's balance isn't enough: you need to see how much the balance dropped each month.

Think of two situations with the same current balance. In the first, you owed 10,000, then 8,000, then 6,000, then 4,000: you drop 2,000 a month. With a 4,000 balance, you have two months left —you're going to pay it off—. In the second, you owed 10,000, then 9,500, then 9,000, then 4,000 this month because you made a big one-time payment that won't repeat; next month you go back to paying 500. With the same 4,000 balance, at 500 a month you have eight months left. Today's balance is identical (4,000), but the payoff date is completely different, and you only know it by looking at the trend —how much you pay in a sustained way—, not the balance of one month.

A migration's burn-down is exactly that account statement. The legacy_calls are the debt balance: what you still owe the legacy. The velocity —how many calls you cut per sprint— is your monthly payment. And the payoff date is the sprint in which legacy_calls reaches zero, which you project by dividing the balance by the payment. Whoever looks only at "I owe 400" (today's balance) doesn't know when they'll finish; whoever looks at "I've been dropping 200 per sprint" (the trend) knows they'll pay it off in two sprints. Measuring a migration's progress is reading its account statement like someone planning to get out of debt: by the slope, not by the day's balance.

Worked example: the burn-down with velocity and projection

We're going to execute the complete burn-down of Mercado's catalog, sprint by sprint, and in each one calculate two things beyond the value: the velocity (how many legacy_calls we burn relative to the previous sprint) and the ETA (the projected closing sprint, estimated at the last sprint's pace). The burn-down data is fixed; the velocity and the projection are derived from it.

# The complete burn-down of catalog's calls to the monolith, sprint by sprint.
# The trend (velocity) matters more than the absolute value: it projects the end.

import math

SAMPLE = 1000
burn_down = [1000, 820, 610, 400, 210, 60, 0]   # legacy_calls per sprint

def traffic_percent(legacy_calls):
    return (SAMPLE - legacy_calls) / SAMPLE * 100

print(f"Burn-down of catalog's calls to the monolith ({SAMPLE} req/sprint)\n")
print(f"{'sprint':>7}{'legacy_calls':>13}{'burned':>10}{'traffic%':>10}"
      f"   ETA (to 0)   burn-down")
print("-" * 78)

for sprint, legacy_calls in enumerate(burn_down):
    tp = traffic_percent(legacy_calls)
    # velocity = how many calls we burned this sprint (delta vs the previous)
    if sprint == 0:
        burned = 0
        eta = "-"
    else:
        burned = burn_down[sprint - 1] - legacy_calls
        # simple projection: at the last sprint's pace, how many left to 0
        if legacy_calls == 0:
            eta = "REACHED"
        elif burned > 0:
            eta = f"~{sprint + math.ceil(legacy_calls / burned)}"
        else:
            eta = "never"
    bar = "#" * (legacy_calls * 24 // SAMPLE)
    print(f"{sprint:>7}{legacy_calls:>13}{burned:>10}{tp:>9.0f}%"
          f"{eta:>11}   |{bar:<24}|")

print("-" * 78)
total_burned = burn_down[0] - burn_down[-1]
avg_velocity = total_burned / (len(burn_down) - 1)
print(f"\n  Average velocity: {avg_velocity:.0f} calls/sprint.  Trend: SUSTAINED drop.")
print("  The value of an isolated sprint (400) doesn't tell if you'll finish; the slope does.")
print("  ETA projects the closing sprint from the velocity: the migration converges to 0.")

What to expect. When you run the file, the output is exactly this:

Burn-down of catalog's calls to the monolith (1000 req/sprint)

 sprint legacy_calls    burned  traffic%   ETA (to 0)   burn-down
------------------------------------------------------------------------------
      0         1000         0        0%          -   |########################|
      1          820       180       18%         ~6   |###################     |
      2          610       210       39%         ~5   |##############          |
      3          400       210       60%         ~5   |#########               |
      4          210       190       79%         ~6   |#####                   |
      5           60       150       94%         ~6   |#                       |
      6            0        60      100%    REACHED   |                        |
------------------------------------------------------------------------------

  Average velocity: 167 calls/sprint.  Trend: SUSTAINED drop.
  The value of an isolated sprint (400) doesn't tell if you'll finish; the slope does.
  ETA projects the closing sprint from the velocity: the migration converges to 0.

Read the legacy_calls column from top to bottom and watch the bar shrink: 1000, 820, 610, 400, 210, 60, 0. That's a burn-down, and it's the most satisfying image of a migration —the evidence, sprint by sprint, that the legacy is dying—. In parallel, the traffic_percent rises from 0% to 100%: every call you stop sending to the legacy is one the new route serves.

Now look at the burned column, which is the velocity —how many legacy_calls you cut relative to the previous sprint—: 180, 210, 210, 190, 150, 60. This column is the one that really tells you whether you'll finish, and why. Notice sprint 3: legacy_calls = 400. If you only looked at that number, you'd know nothing —is 400 going well or badly?—. But the burned column says 210: you've been burning around 200 calls per sprint, sustained. With a balance of 400 and a velocity of 200, you have two sprints left. That's the power of the trend: it turns a mute number (400) into a prediction (you finish near sprint 5-6).

The ETA column makes that projection explicit: in each sprint, it estimates the closing sprint assuming you keep the last one's pace. In sprint 1 it projects ~6; in sprints 2 and 3, ~5; and at the end, in sprint 6, it marks REACHEDlegacy_calls hit zero—. The ETA moves a little sprint by sprint (because the velocity isn't perfectly constant), but stays in a narrow range of 5 to 6: the migration converges. A healthy migration has a stable or approaching ETA; a migration in trouble has an ETA that moves away sprint by sprint (each time projecting a more distant end) or that says never (velocity zero: you got stuck).

The final line summarizes the average velocity: 167 calls per sprint, with a sustained downward trend. That steady slope is the signature of a migration that's going to finish. The lesson repeats it twice because it's the whole point: the value of an isolated sprint (400) doesn't tell whether you'll finish; the slope does.

Deep dive: reading the shape of the burn-down

Today's value is a snapshot; the shape of the curve is the movie, and each shape tells a different story about whether the migration is going to finish. It's worth recognizing the four typical shapes, because diagnosing the burn-down by its shape is the central skill of this lesson.

  The converging one       The stuck one           The never-starting one    The staircase one
  (healthy, reaches 0)     (eternal migration)     (all effort, 0 cut)       (advances in jumps)

  #                        #                        # # # # # #               # #
  ##                       ##                       # # # # #                 # #
  ####                     ####                     # # # #                   # # # #
  ######                   ######___                # # #                     # # # #
  ########____             ######                   # #                       # # # #____
  0                        (stays there)            # (doesn't drop)          0

  sustained velocity       velocity -> 0 near       velocity = 0 always       velocity in bursts,
  stable ETA               the end; ETA -> never    the legacy doesn't die    but converges
  • The converging one (the example's): the velocity holds, the curve drops steadily to zero, the ETA is stable. It's the healthy migration. You don't have to do anything but keep going.
  • The stuck one: it drops well at first and then the velocity falls to almost zero near the end —typically in the last 5-10%—. The curve flattens before reaching zero, the ETA starts saying "never". It's the pattern of the eternal migration (lesson 7): 90% is done fast, the last 10% never gets done because the team lost interest or the remaining cases are the rare and difficult ones. The early sign is the velocity falling while legacy_calls is still greater than zero.
  • The never-starting one: legacy_calls doesn't drop even though the team works. It's the migration that confuses effort with result (lesson 2): a lot of activity, zero cutting. The velocity is zero from the start. The sign is a flat burn-down while the story points rise.
  • The staircase one: it advances in jumps (one sprint cuts a lot, another nothing) because the work comes in blocks —for example, migrating a whole endpoint at once—. It can be perfectly healthy if the general trend converges; you just have to average the velocity over several sprints instead of looking sprint by sprint.

The projection (ETA) deserves an honest nuance: it's an extrapolation, and like every extrapolation, it assumes the pace holds. In reality, the last stretch of a migration is usually the slowest —the cases left at the end are the rare, the difficult, the ones nobody wanted to touch—, so the ETA tends to be optimistic near the close. Notice in the example: the velocity falls from 210 to 150 to 60 in the last sprints. That's not a problem if the curve keeps dropping, but it's the reason a migration "at 95%" can take as long as the previous 95%: the final 5% is of another nature. Reading that in the burn-down —the velocity falling near the end— is what lets you anticipate the stall before it becomes eternal.

A detail about what to count in legacy_calls. In this module's model we count requests that touch the legacy over a fixed sample of 1000, which is the most direct. But "calls to the legacy" can be measured in several complementary ways, and in a real migration it's worth looking at more than one: requests served by the old route (the strangler's traffic_percent), calls the new code makes back to the monolith (internal dependencies you haven't cut yet), and queries to the still-shared tables. Each is a different burn-down, and the migration doesn't finish until all reach zero —a point lesson 6 develops when defining the done with several conditions—. Here we use the aggregate burn-down as the main metric; remember that underneath there may be several curves that must also touch the floor.

Common mistakes

Reading the value of a sprint instead of the trend. What happens: the team looks at legacy_calls = 400 and discusses whether that's good or bad, without looking at where it came from or at what pace it drops. Why it happens: today's value is the most visible number and the easiest to report; the trend requires comparing sprints. How to spot it: the progress conversations revolve around the current number ("we're at 400") and not around the slope ("we've been dropping 200 per sprint"); nobody can say the ETA. How to fix it: measure and report the velocity (how many legacy_calls were cut relative to the previous sprint) and the ETA derived from it. The value of 400 is identical in a migration that converges (came from 610) and in one that got stuck (came from 410); only the trend distinguishes them. The burn-down is a movie, not a snapshot.

Projecting the end date with the velocity of the first sprints. What happens: the team sees that in the first sprints it burns 200 calls each, extrapolates in a straight line, and promises an end that arrives early. Why it happens: linear extrapolation is the simplest, and the first sprints are usually the fastest (the easy cases are migrated first). How to spot it: the ETA promised at the start turns out too optimistic; the last stretch takes much longer than projected. How to fix it: remember that the last stretch is slower —the cases left at the end are the rare and difficult ones, and the velocity falls (in the example, from 210 to 60)—. Project with the recent velocity, not that of the first sprints, and treat the ETA as a range that adjusts, not a fixed promise. And watch for the stall sign: if the velocity falls while legacy_calls is still far from zero, the ETA is moving away and you have to act before it becomes "never".

Celebrating the 90% as if it were the end. What happens: the burn-down reaches 90% fast (legacy_calls dropped from 1000 to 100), the team reads it as "almost there", and eases up. Why it happens: 90% feels finished, and the curve dropped so well that it seems the rest will be equally fast. How to spot it: the velocity falls right after 90%, the curve flattens, and the remaining legacy_calls stays weeks in the same range. How to fix it: the last 10% is usually of another nature than the first 90% —they're the rare cases, the flows nobody documented, the forgotten integrations—, and it can cost as much as everything before. It's not 90% of the work done; it's 90% of the traffic cut, which is different. The migration ends at zero, not at 90%, and the burn-down reminds you by showing the bar isn't empty yet. Lesson 7 is entirely about what happens when you give in to this temptation: the eternal migration stuck in the last stretch.

Exercises

Exercise 1 — Snapshot vs movie. Two migrations report today legacy_calls = 300. Migration A came from 900, 600, 300 (last three sprints); B came from 340, 320, 300. (a) What's the velocity of each? (b) Project the ETA of each (sprints to reach 0 at the current pace). (c) Why does the same value of 300 mean opposite things?

See solution

(a) Migration A: dropped from 600 to 300 in the last sprint → velocity 300 calls/sprint. Migration B: dropped from 320 to 300 → velocity 20 calls/sprint.

(b) A: with a 300 balance and velocity 300, 300 / 300 = 1 → finishes in ~1 more sprint. B: with a 300 balance and velocity 20, 300 / 20 = 15 → finishes in ~15 more sprints (if the velocity doesn't fall further). A converges; B is practically stuck.

(c) Because today's value (300) is a snapshot that doesn't contain the information that matters: the slope. Migration A has been dropping 300 per sprint —it's one sprint from finishing—; B has been dropping 20 per sprint —it's 15 sprints away, and probably on its way to the eternal migration—. The same balance with different payments gives opposite payoff dates. Only the trend (velocity) distinguishes a migration that converges from one that got stuck, and that's why the burn-down is read like a movie, not like a snapshot.

Exercise 2 — Diagnose the shape. For each burn-down (sequence of legacy_calls per sprint), say what shape it has and whether the migration is going to finish: (a) 1000, 700, 450, 250, 100, 0; (b) 1000, 850, 750, 720, 715, 713; (c) 1000, 1000, 1000, 990, 1000, 995; (d) 1000, 1000, 400, 400, 100, 100, 0.

See solution

(a) 1000→0 with a sustained velocity (300, 250, 200, 150, 100) that drops but never turns off, and reaches zero. It's the converging one: healthy migration, finishes. (Note the velocity decreases toward the end —the last stretch is slower—, but the curve does touch the floor.)

(b) 1000→713 and flattens there: velocity 150, 100, 30, 5, 2 —falls to almost zero near 70%—. It's the stuck one: the curve stopped far from zero, the ETA tends to "never". It's the pattern of the eternal migration; you have to intervene before it stays there forever.

(c) It stays glued at ~1000, without dropping (the small variations are noise). It's the never-starting one: velocity zero, the legacy doesn't die. Probably the team works (effort) but doesn't cut calls (result); lesson 2 in action.

(d) It drops in jumps: two flat sprints, then cuts 600 at once, two flat, cuts 300, flat, cuts 100 to zero. It's the staircase one: it advances in blocks (migrating whole endpoints at once). It does finish —the general trend converges to zero—; you just have to average the velocity over several sprints instead of panicking at the flat sprints.

Exercise 3 — The ETA that moves away. A migration projects these ETAs sprint by sprint: sprint 1 → ~8, sprint 2 → ~9, sprint 3 → ~11, sprint 4 → ~15. (a) What's happening with the velocity? (b) Is this migration healthy? (c) What would you do on seeing this pattern, and why is it better to detect it now than in sprint 15?

See solution

(a) The velocity is falling sprint by sprint. An ETA that moves away (8 → 9 → 11 → 15) can only happen if each sprint cuts fewer calls than the previous: the balance drops slower, so dividing it by a smaller velocity gives a longer term. The projection moves away because the pace is deflating.

(b) No. A healthy migration has a stable or approaching ETA (like the text's example, which stayed at 5-6). An ETA that moves away is the early signature of a migration on its way to getting stuck —the "stuck one" shape—: if the velocity keeps falling, the ETA will tend to "never" and the migration will end up eternal at some point before zero.

(c) On seeing this pattern, I'd investigate why the velocity falls and act now: are the remaining cases the rare and difficult ones (expected, but you have to plan time for them)? did the team get distracted with other work (regain focus)? is there a technical blocker in the last stretch (resolve it)? It's better to detect it in sprint 4 than in sprint 15 because in sprint 4 the migration still has momentum and budget and attention; in sprint 15, with the ETA blown up and the team tired, is exactly when migrations get abandoned "at 90%" and become eternal. The ETA that moves away is an early alarm; lesson 7 covers what to do so as not to let the migration die of old age in that last stretch.

Summary and next step

In this lesson you built the module's central instrument: the burn-down of the calls to the legacy. You saw, with the debt you pay off month by month and the payoff date you only know by the trend, that what matters isn't today's balance (legacy_calls = 400) but the slope with which it drops (the velocity) —the same value means "you finish in two sprints" or "you got stuck" depending on where you came from—. And you executed it: the curve 1000→820→...→0 with its traffic_percent rising to 100%, the burned-calls column as velocity, and the ETA projecting a stable close at sprint 5-6. You learned to read the four shapes of the burn-down (converges, gets stuck, never starts, staircase), to project with the recent velocity and not that of the first sprints, and to recognize the early sign of the stall: the velocity falling or the ETA moving away while legacy_calls is still far from zero.

Before moving on you should be able to: calculate the velocity and the ETA of a burn-down; distinguish a migration that converges from one that gets stuck by the shape of its curve; explain why the value of an isolated sprint doesn't tell whether you'll finish; and anticipate that the last stretch is slower than the first.

Lesson 4 adds the dashboard's second instrument, one that watches a danger the burn-down doesn't see: while you reduce the legacy at the front (cutting calls), someone could be adding code to it from behind (a new feature built on the monolith you're killing). The burn-down would drop the same, but the legacy would be growing. The migration fitness function is the alarm that catches that: a test that counts the references to the legacy and fails if they grew. You're going to write it, see it give PASS when the legacy shrinks and FAIL —break the CI— when someone puts new code into the monolith.

Resources

  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), ch. 3 — Newman describes measuring the progress of the monolith's decomposition by the functionality and calls retired from it; this module's burn-down is that idea made a graph. In English.
  • Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. The pattern whose sign of success is that the old system serves less and less until it serves nothing: exactly what the burn-down measures sprint by sprint. In English.
  • Neal Ford, Rebecca Parsons, and Patrick Kua, Building Evolutionary Architectures (O'Reilly, 2nd ed., 2022) — on measuring properties of the architecture objectively and tracking them over time; the spirit of treating the progress as a metric that's graphed and projected, not as a perception. In English.
  • Martin Fowler, martinfowler.com — the bliki with the entries on strangler fig and evolutionary architecture that ground why a migration's progress is measured as a curve dropping toward zero. In English.