Module 5: Extracting a Service

Project: extract Mercado's catalog into a service

Overview

You reached the module's capstone. In the previous lessons you built the extraction piece by piece: finding the bounded context by measuring the seam (L2), the anti-corruption layer that translates the old model to the clean one (L3), the ACL in both directions (L4), the ownership of the data (L5), the phased cut of the shared DB (L6), and the extraction order (L7). In this project you put it all together in a single executed simulation: you extract Mercado's catalog —the first of your plan— end to end, run as an extraction journal in phases.

The journal is the most honest way to see a complete extraction, because it shows what the isolated lessons don't: how the pieces work together over time, and —above all— how the monolith responds identical in every phase while its guts change completely. You'll see the catalog go from being an internal function of the monolith (which reads the table directly, in the old model) to being a service with its clean model (Product), its ACL at the edge, and its own database (owned_db) —and in the four phases, the monolith receives exactly the same thing—. It's the whole movie of an extraction, not the loose snapshots.

And this project has a deliverable, like every capstone: (1) the extraction plan by bounded contexts —why the catalog first, and in what order the rest—, (2) the executed code —the complete extraction journal run with its literal output—, and (3) the justification of why extracting with an ACL and its own data beat the alternative of sharing the model and the tables. When you finish, you'll hold in your hands an extraction of a real bounded context, from start to end, that you can defend.

Connection with the module. This project integrates lessons 2 to 7 into one continuous execution. It closes module 5 and prepares the next: module 6 (migrating data without downtime) takes the data copy we resolve here in one line (shared_db → owned_db) and executes it for real, with dual-write, backfill, and parallel-run, for millions of records and with writes arriving during the move. Notice the capstone's boundary: here we extract the catalog with the extraction technique (bounded context, ACL, ownership, DB cut). Where the service ends up (microservice, events, API) belongs to the styles guides; migrating its data without downtime in depth is module 6; and measuring the progress of the complete migration of Mercado (all the contexts) is module 7. This capstone is one extraction, executed whole.

The extraction plan by bounded contexts

Before the code, the plan —because an extraction without a plan is just moving code—. Mercado is a monolith with four bounded contexts: catalog, orders, payments, shipping. We don't extract them all at once; we choose the most independent and take it end to end before touching the next. Lesson 7 already made this choice with numbers; here we execute it.

Context      Why this order                                  Status in this project
──────────  ─────────────────────────────────────────────  ──────────────────────
catalog      leaf: 0 outbound, many inbound, writes no        <- THIS context,
             shared state; the loose end of the knot            end to end
payments     lightly coupled (1 outbound), handles money:      next (risk
             risk may defer it in the sequence                  may defer it)
shipping     2 outbound; simplifies once catalog leaves        later on
orders       the hub (3 outbound): extracted last, when        last
             its dependencies are already services

The plan's principle: from the leaves toward the hub, one at a time and end to end. Don't open the extraction of orders while the catalog's is half done —you'd end up with four incomplete extractions, no service owning its data—. You take the catalog until it owns its owned_db and cuts the shared_db, you collect the prize (one bounded context less of monolith), and then you start the next one. Each complete extraction reduces the monolith and loosens the knot for the next.

An analogy: the eldest child becomes fully independent

Return to the module's family, but now follow a single child —the eldest, the most independent— on their complete journey from the house to self-sufficiency. You don't move them all at once; you accompany them through the stages, and in each one they're a bit more the owner of their life, while the family (the monolith) keeps working the same.

At first they live at home: their room is part of the house, their expenses go on the family bill, and when you speak to them, you speak in the same old language. It's the catalog as an internal function of the monolith: inside the code, over the shared table, in the old model.

Then they move out and learn another language, but you place a translator: they rent their apartment, start speaking their own clean language, but so the family can still understand each other with them you hire an interpreter who translates in both directions. The family speaks to them as always; the interpreter translates; the child answers in their language; the interpreter translates back. Nobody had to learn the other's language. It's inserting the bidirectional ACL: the service already speaks Product, but the monolith still receives its old model.

Then they open their own bank account: they stop using the family card and manage their own money. It's giving them the ownership of the data: the service stops reading the monolith's table and starts owning its store.

And finally they buy their house and cut the last tie: they no longer depend on anything from the family house; if they coordinate something, they do it through an explicit transfer. It's the cut of the shared DB: shared_db → owned_db, the monolith's table retired.

In the four stages, the family worked the same: nobody else was left without food or shelter because the eldest child became independent. That "the family works the same" is the column the journal verifies: monolith resp. same: YES in every phase. This capstone accompanies the eldest child —the catalog— on their complete journey, from living at home to having their own house and their own accounts.

Worked example: the catalog extraction journal, executed

Here is the complete extraction of the catalog, integrating the module's pieces, run as a journal in phases. The SharedDB is instrumented (it records who touches it); the CatalogService changes its data source according to the phase; the ACL (to_modern/to_legacy) translates at the edge; and in each phase the monolith requests product 1 and we compare its response against the baseline —what it received before starting the extraction—.

from dataclasses import dataclass

@dataclass
class Product:
    id: int
    name: str
    price_cents: int
    active: bool

# --- The shared legacy table. Instrumented: records the monolith's accesses. ---
class SharedDB:
    def __init__(self):
        self.products = [
            {"prod_id": 1, "desc": "SSD 1TB",  "prc_cents": "8999", "act": "Y"},
            {"prod_id": 2, "desc": "USB-C Hub", "prc_cents": "3200", "act": "Y"},
        ]
    def read(self, prod_id):
        return next(r for r in self.products if r["prod_id"] == prod_id)

shared_db = SharedDB()

# --- The ACL: translates in both directions at the service edge. ---
def to_modern(row):
    return Product(row["prod_id"], row["desc"], int(row["prc_cents"]), row["act"] == "Y")

def to_legacy(p):
    return {"prod_id": p.id, "desc": p.name, "prc_cents": str(p.price_cents),
            "act": "Y" if p.active else "N"}

# --- The catalog service. Its behavior depends on the extraction phase. ---
class CatalogService:
    def __init__(self, phase):
        self.phase = phase
        self.owned_db = {p.id: p for p in (to_modern(r) for r in shared_db.products)}
        self.reads_monolith_table = 0     # coupling: times it touches shared_db

    def get(self, prod_id):
        if self.phase in (1, 2):
            self.reads_monolith_table += 1
            return to_modern(shared_db.read(prod_id))   # still via shared_db
        return self.owned_db[prod_id]                    # phase 3+: its own DB

# --- Phase 0: baseline. The catalog is an INTERNAL function of the monolith (legacy). ---
def legacy_catalog_internal(prod_id):
    return shared_db.read(prod_id)          # the monolith reads the table directly

def monolith_response(prod_id, phase):
    if phase == 0:
        return legacy_catalog_internal(prod_id)         # old model, direct table
    svc = CatalogService(phase)
    resp = to_legacy(svc.get(prod_id))                  # the ACL returns legacy to it
    return resp, svc.reads_monolith_table

BASELINE = legacy_catalog_internal(1)       # what the monolith saw before extracting

phases = [
    (0, "monolith internal", "legacy", "monolith", "extract?"),
    (1, "ACL + shared_db",   "Product","service",  "extract?"),
    (2, "ACL + shared_db",   "Product","service",  "extract?"),
    (3, "ACL + owned_db",    "Product","service",  "extract?"),
]

print("Extraction journal of Mercado's catalog (monolith identical in every phase)\n")
print(f"{'phase':>5}  {'what was done':<30}{'internal model':<16}"
      f"{'hits shared_db':>15}{'same resp.?':>13}")
print("-" * 81)

descs = {
    0: "baseline: internal function",
    1: "bounded context + ACL",
    2: "the service owns its data",
    3: "cut: shared_db -> owned_db",
}
for phase, _a, model, _b, _c in phases:
    if phase == 0:
        resp = monolith_response(1, 0)
        touches = "yes"
        same = (resp == BASELINE)
    else:
        resp, reads = monolith_response(1, phase)
        touches = "yes" if reads > 0 else "no"
        same = (resp == BASELINE)
    print(f"{phase:>5}  {descs[phase]:<30}{model:<16}{touches:>15}{('YES' if same else 'NO'):>13}")

print("-" * 81)
print(f"\nResponse the monolith received in the 4 phases: {BASELINE}")

# --- Final state: the service is really extracted. ---
final = CatalogService(3)
p = final.get(1)
final_legacy = to_legacy(p)
print("\nFinal state of the extracted catalog:")
print(f"  model the service speaks       : {p}   (clean: Product)")
print(f"  data source                    : owned_db (its own)")
print(f"  accesses to the monolith table : {final.reads_monolith_table}")
print(f"  what the monolith still sees   : {final_legacy}   (via ACL, unchanged)")
print("\n  Catalog is now out of the monolith: its own bounded context, clean model,")
print("  its own data, and the ACL at the edge translating. One slice less of monolith.")

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

Extraction journal of Mercado's catalog (monolith identical in every phase)

phase  what was done                 internal model   hits shared_db  same resp.?
---------------------------------------------------------------------------------
    0  baseline: internal function   legacy                      yes          YES
    1  bounded context + ACL         Product                     yes          YES
    2  the service owns its data     Product                     yes          YES
    3  cut: shared_db -> owned_db    Product                      no          YES
---------------------------------------------------------------------------------

Response the monolith received in the 4 phases: {'prod_id': 1, 'desc': 'SSD 1TB', 'prc_cents': '8999', 'act': 'Y'}

Final state of the extracted catalog:
  model the service speaks       : Product(id=1, name='SSD 1TB', price_cents=8999, active=True)   (clean: Product)
  data source                    : owned_db (its own)
  accesses to the monolith table : 0
  what the monolith still sees   : {'prod_id': 1, 'desc': 'SSD 1TB', 'prc_cents': '8999', 'act': 'Y'}   (via ACL, unchanged)

  Catalog is now out of the monolith: its own bounded context, clean model,
  its own data, and the ACL at the edge translating. One slice less of monolith.

Read the journal phase by phase, because each row is a step of the extraction and each column one of the module's pieces working together.

Phase 0 — The starting point: catalog is an internal function of the monolith. The monolith reads the table directly, in the old model (internal model: legacy), and touches the shared_db (yes). There's no service, no ACL. It's the state before touching anything, the baseline against which everything else is compared. same resp.: YES is trivially true here (it's the baseline itself).

Phase 1 — What lessons 2 to 4 cover is applied: catalog is recognized as a bounded context and given the ACL. The service's internal model becomes Product —from the ACL inward, the service speaks clean—. But notice: hits shared_db: yes. The service already exists with its clean model, but its data is still in the shared table (transitional phase of lesson 6). And the crucial thing: same resp.: YES. The monolith receives exactly the same as in phase 0, even though inside there's now a service with another model and a translator in between. The return ACL kept the contract.

Phase 2 — Lesson 5: the service starts to own its data, conceptually —the internal model is Product, the service is the one that responds—, but in the journal it still hits shared_db: yes, because the copy to its own DB hasn't happened yet. It's the stage where the service is already the logical owner of the catalog (nobody else should write its products), even though physically its data is still in the shared table. Again, same resp.: YES: the monolith notices nothing.

Phase 3 — The final cut of lesson 6: shared_db → owned_db. The service now reads from its own database (hits shared_db: no), the data copy was done, and the shared table is retired. And —the seal— same resp.: YES. After moving the model, the ownership, and the data, the monolith still receives identical what it received in phase 0.

The bottom line confirms it in a single check: Response the monolith received in the 4 phases: {'prod_id': 1, 'desc': 'SSD 1TB', 'prc_cents': '8999', 'act': 'Y'}. A single response, for the four phases. Inside, catalog traveled from internal function to self-sufficient service; outside, the monolith couldn't tell the difference.

And the final state closes the story: the service speaks Product(id=1, name='SSD 1TB', price_cents=8999, active=True) (clean model), its data source is owned_db (its own), it touched the monolith's table 0 times (truly extracted, per the detector of lesson 5), and the monolith still sees {'prod_id': 1, 'desc': 'SSD 1TB', ...} (its old model, via ACL, unchanged). catalog ended up outside the monolith: its own bounded context, clean model, its own data, ACL at the edge. One slice less of monolith.

The capstone deliverable

A capstone delivers artifacts, not just understanding. Here are the three:

1. The extraction plan by bounded contexts. Mercado's monolith is extracted context by context, starting with catalog (the leaf: 0 outbound, many inbound, writes no shared state) and continuing from the leaves toward the hub —payments, shipping, orders—, with the risk of payments's money as a factor that can defer it in the sequence. Each context runs through the four phases of the extraction until it owns its owned_db before starting the next. (The plan's table is above.)

2. The executed code. The complete extraction journal —bounded context + bidirectional ACL + ownership of the data + cut of the shared DB— run with its literal output, the four phases. It's not pseudocode or a description: it's the simulated extraction, reproducible, that you can run and modify (change the model, add a field, break the return ACL on purpose and watch same resp. drop to NO).

3. The justification: why extracting with an ACL beat sharing the model and the tables. The tempting alternative was to make the "service" speak the old model and read the monolith's tables —it seemed faster—. This project shows why the real extraction wins: the service ended up with a clean model (its logic reads by itself, price_cents == 0, not int(row["prc_cents"]) == 0), with its own data (it can evolve its schema without breaking anyone, 0 accesses to the old's table), and with the monolith intact (it responded identical in the four phases, without rewriting its hundreds of consumers). Where sharing the model and the tables would have given a "service" born with the monolith's debt and chained to its DB, the extraction with an ACL gave a self-sufficient piece that can live its own life. That's the justification, and now you can back it with an executed journal.

Common mistakes

Declaring the extraction finished at phase 1 (there's a service) without reaching phase 3 (its own data). What happens: on seeing the service working with its ACL and its clean model (phase 1), the team considers the extraction done —but the service keeps reading the shared table—. Why it happens: phase 1 feels like the achievement (there's a service now!), and the data cut (phases 2-3) is work without visible reward. How to spot it: the service has been in production for months but still hits shared_db: yes; its owned_db was never created. How to fix it: the extraction —like the strangler— finishes when the service owns its data (phase 3), not when it exists (phase 1). A service with an ACL but without its own data is the child with their own apartment but with the family card: it looks independent, it's still chained. The slice is extracted when it cuts the shared_db, not when the service appears.

Extracting several contexts at once instead of finishing one. What happens: excited, the team opens the extraction of catalog, orders, and payments at the same time. Why it happens: it seems faster to advance on everything at once, and phase 1 of each feels cheap. How to spot it: there are three or four extractions "in progress", none near owning its data; the monolith didn't shrink because no context cut its shared_db. How to fix it: one context at a time, end to end, from the leaves toward the hub. Finish the catalog —until it owns its owned_db— before touching the next. The progress of an extraction is measured not in "how many contexts you started" but in "how many own their data". Four extractions at phase 1 are worth zero extracted contexts; one at phase 3 is worth one. And finishing one loosens the knot for the next (the cumulative effect of lesson 7).

Not verifying same resp. in every phase. What happens: the team advances the phases (puts in the ACL, moves the data) without comparing, at each step, the response the monolith receives against the baseline. Why it happens: "the ACL translates, surely it's fine". How to spot it: a subtle bug reaches production —a field that got lost when moving the data, a format that changed— because nobody compared. How to fix it: every phase is verified with the same resp. column of the journal: compare the monolith's response against the baseline and confirm it's YES. It's the facade's transparency test (module 3) and the ACL round-trip (lesson 1), applied to the complete extraction. The ACL should keep the contract in the four phases, but "should" isn't "was verified". The same resp. column is what turns the promise "the monolith doesn't notice" into a measured fact, phase by phase.

Exercises

Exercise 1 — Explain phase 3. In the journal, phase 3 is the only one where hits shared_db drops to no. (a) What was done in that phase that hadn't been done before? (b) Why did same resp. stay at YES despite the change? (c) Why is this the phase that "collects" the extraction?

See solution

(a) In phase 3 the cut of the shared DB was done: the data was copied to the service's owned_db, the service started reading from there instead of the shared_db, and the shared table was retired. It's the step where the service stops depending on the monolith's table and starts owning its data physically —what phases 1 and 2 hadn't done yet, because there the service existed with its ACL but kept reading the shared table (hits shared_db: yes)—.

(b) Because the ACL at the edge kept the monolith's contract identical. The data source changed inside (from shared_db to owned_db), but the return ACL (to_legacy) kept handing the monolith its exact old model —{'prod_id': 1, 'desc': 'SSD 1TB', 'prc_cents': '8999', 'act': 'Y'}—. The monolith requests, the ACL translates, and the monolith receives the usual, no matter which store the data came out of. That's the ACL's job: absorb the topology change below and keep the contract above.

(c) Because it's the phase where the service truly owns its data —the second cut of the extraction, the data one, which is what makes the service self-sufficient—. Until phase 2, the service had a clean model and an ACL, but stayed chained to the monolith's table: if the monolith changed that table, the service broke. In phase 3, with owned_db and 0 accesses to the old table, the service can evolve its schema without breaking anyone and the monolith shrank (one table less to share). Without phase 3, the extraction is half done; with it, the prize was collected: one bounded context less of monolith.

Exercise 2 — Break the ACL and predict. Without running the code, predict what would happen in the journal if the return ACL (to_legacy) had a bug: it returned prc_cents as an int (8999) instead of a string ('8999'). (a) Which column of the journal would change? (b) In which phases? (c) What does this tell you about what the same resp. column is for?

See solution

(a) The same resp.? column would change: it would drop from YES to NO. The baseline (phase 0) has prc_cents as the string '8999' (the monolith's real old model), but the buggy ACL would return 8999 as an int. The comparison resp == BASELINE would fail, because {'prc_cents': '8999'} is not equal to {'prc_cents': 8999} —different types—.

(b) In phases 1, 2, and 3 —all the ones that go through the ACL—. Phase 0 doesn't use the ACL (it's the monolith's direct internal function, the baseline), so there same resp. would stay at YES (it's compared against itself). But as soon as the ACL comes into play (phase 1 onward), the to_legacy bug would make the response differ from the baseline, and same resp. would give NO in the three remaining phases.

(c) That the same resp. column is the extraction's safety net: it catches any moment when the monolith stops receiving exactly what it received before. A bug in the return ACL —as subtle as a changed type— would break the monolith's consumers in production, but the journal catches it before, comparing against the baseline in every phase. Without that column, the bug would slip in silently (the service "works", returns a correct Product) until a consumer of the monolith that expected prc_cents as a string broke. same resp. turns the promise "the monolith doesn't notice" into an executed verification, and that's why it's checked in every phase, not just at the end.

Exercise 3 — The plan for the next extraction. The catalog was extracted (phase 3, its own data). Now it's the next context's turn. (a) According to the plan, which comes next and why? (b) What of this catalog extraction would you reuse, and what would you expect to be different? (c) How did extracting catalog first loosen the knot?

See solution

(a) According to the plan (lesson 7), the next by coupling is payments (1 outbound, score 3), although its handling of money could defer it in the sequence for risk —a prudent team could go for shipping or another low-risk context before touching the money—. The skeleton doesn't change: the leaf (catalog) already left, the hub (orders) goes last; the middle is tuned by value and risk, and that's recorded in the ADR.

(b) You would reuse the entire extraction technique: finding the bounded context by measuring the seam, the bidirectional ACL pattern, the data-ownership detector, and the phased cut of the shared DB with same resp. verification. The technique is the same for any context. You would expect it to be different: payments writes shared state (write_heavy), unlike the catalog which is almost only read —so the ownership of its data and the DB cut are more delicate (there are writes to coordinate, not just reads)—; its ACL will translate a different model; and its ramp will be more careful because of the money risk. The choreography is the same; the content and the care change.

(c) Extracting catalog first loosened the knot for the contexts that depended on it —shipping and orders, which read it—. Before, that dependency pointed into the monolith; now it points toward a service with a clean API, easier to handle. When it's shipping's turn to be extracted, one of its two dependencies is already a well-defined service, so its seam is simpler than at the start. That's the cumulative effect of lesson 7: each extracted leaf simplifies the seam of those that remain, until the hub —impossible at the start— becomes reachable at the end. Starting with the leaf wasn't just the easy thing; it was what made everything that follows easier.

Summary and next step

In this capstone you integrated the whole module into a single executed extraction: you took Mercado's catalog —the first of your plan— from an internal function of the monolith to a self-sufficient service, run as a journal of four phases. You saw the complete system work together: the bounded context recognized, the bidirectional ACL placed, the ownership of the data taken, and the shared DB cut (shared_db → owned_db), with the monolith responding identical in the four phases —a single response for all—. And you produced the capstone deliverable: the extraction plan by bounded contexts, the executed code, and the justification —backed by the journal— of why extracting with an ACL and its own data beat sharing the model and the tables.

With this you close module 5. You master the extraction technique: you know how to find the bounded context by measuring the seam, put an ACL on it that translates old↔new in both directions, give it ownership of its data, cut the shared DB in phases without turning off the system, and choose the extraction order from the leaves toward the hub.

What follows completes the data cut we left flagged here. In this capstone, the copy from shared_db to owned_db was a single line of code —because the focus was ownership, not the move—. Module 6 (migrating data without downtime) executes that move for real: how to move millions of records from the old store to the new one while the system keeps reading and writing, with expand-contract (add the new, migrate, remove the old), dual-write (write to both during the transition), backfill (fill in the historical), and parallel-run (read from both and compare before trusting). The extraction gave you the piece with its own data; module 6 gives you how to fill that own data without the business stopping.

Resources

  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), ch. 3 and 4 complete — the capstone's integral reference: extracting a service (choosing the context, the ACL, keeping the contract) and decomposing its database (the ownership of the data, cutting the shared DB in phases). The map of this extraction and the ones that follow. In English.
  • Eric Evans, Domain-Driven Design (Addison-Wesley, 2003), chapters on Bounded Context and Anti-Corruption Layer — the two concepts the capstone executes from start to end. Reread them now that you have the complete mechanics: every sentence will have a concrete referent. In English.
  • Chris Richardson, "Pattern: Database per service" — microservices.io/patterns/data/database-per-service.html. The destination of the extraction: each service with its own database, and the monolith one slice smaller with each context that leaves. In English.
  • Martin Fowler, "StranglerFigApplication" (2004) — martinfowler.com/bliki/StranglerFigApplication.html. The sibling pattern that diverts traffic to the extracted service; this module's extraction and module 3's strangler combine into a real, incremental, and reversible migration. In English.