Module 2: Conway's Law

5. The modules that cross team boundaries

Overview

By the end of this lesson you'll be able to point out, with a number, which module of a system is the one that most slows it —and why the blame is never on the people, but on the structure—. The guilty module is the one that crosses team boundaries: the one two or more squads consider theirs and touch all the time, so that every change needs several teams to agree. In Mercado that module is checkout: orders considers it its own (it's the end of the purchase flow), but payments gets its hands into it constantly (checkout is where charging happens), and in practice shipping and platform also touch it (checkout triggers shipping and notifications). A module like that is the point where Conway's tax —which lesson 4 taught you to see— concentrates: each pair of teams that has to coordinate to change it is an inter-team channel of the most expensive kind, and checkout forces four teams to be in the room at once. You'll measure that friction with a precise metric —the pairs of teams that must coordinate to change each module, C(k,2)— and see that checkout, alone, contributes half of all the friction in the system.

This matters because it changes whom and what you blame when an area of the system is slow and fragile. The instinct is to blame the people: "the checkout team is disorganized", "they don't coordinate well". The measurement shows the opposite: friction isn't a defect of the people, it's a property of the structure. Put the best engineers in the world to maintain a module four teams own at once, and they'll be slow —not because they're incapable, but because every decision requires a four-team meeting with different priorities—. The crossed module generates friction the same way an intersection without a traffic light generates crashes: it doesn't matter how good the drivers are, the geometry of the crossing is the problem. And this diagnosis is what enables the cure: once you can measure that checkout concentrates 50% of the friction by crossing four teams, you know exactly where to apply the inverse maneuver of lesson 6 —giving it a single owner so its coordination stops being inter-team and becomes internal—.

Connection with the module: this lesson is the fine diagnosis. Lesson 2 measured the global misalignment (61.5% cross-team); lesson 3 explained its origin (the one-team monolith divided among five); lesson 4 gave the economic why (inter-team coordination is expensive). This one joins them and points them at a single module: which is the one that hurts the most, and how much? It refines the lesson 2 metric —instead of counting cross-team dependencies plainly, it weights how many teams must coordinate simultaneously to change each module, which better captures the real pain—. And it delivers the precise target for the cure: the number you produce here (checkout = 6 of 12 coordination pairs) is the "before" that the inverse maneuver of lesson 6 will lower. If lesson 4 was "cross-team coordination is expensive", this one is "and here's the module that concentrates it, measured".

The kitchen shared by four restaurants

Think of it this way. In a food court there are four restaurants: a taquería, a pizzeria, a café, and a juice bar. To save money, the four share a single kitchen. Each restaurant has its own menu, its own owner, its own rhythm. But all cook in the same space, with the same stoves, the same fridge, the same sink.

What happens every time one wants to change something? The taquería wants to move the griddle to make more space: it can't decide alone, because the griddle gets in the pizzeria's way. The café wants to reorganize the fridge: it has to consult the other three, because they all store their supplies there. Buying a new stove, changing the cleaning schedule, rearranging the sink —any change, however small, requires the four owners to agree—. And agreeing among four owners with different priorities is slow and tense: the taquería is in a hurry, the juice bar doesn't want to spend, the pizzeria agrees but only if it's after lunch. A kitchen shared by four isn't four times harder to coordinate than an own kitchen; it's six times harder, because there are six pairs of owners who can disagree (taquería-pizzeria, taquería-café, taquería-juice bar, pizzeria-café, pizzeria-juice bar, café-juice bar).

Compare with the restaurant next door, which has its own kitchen. When it wants to move the griddle, it moves it. When it wants to change the fridge, it changes it. Zero meetings, zero negotiations. A single owner decides and executes. That restaurant is fast not because its chef is better, but because no one else shares its kitchen.

Mercado's checkout is the kitchen shared by four. Orders, payments, shipping, and platform all touch it, so any change to checkout is a four-team meeting —six pairs to negotiate—. And like in the food court, the slowness isn't the fault of any particular team: it's the geometry of sharing. This lesson measures that geometry, and shows that the solution isn't "coordinate better" (impossible to improve six simultaneous negotiations) but "give checkout its own kitchen" —a single owner—, which is the inverse maneuver of lesson 6.

Worked example: the friction per module, measured

We'll refine the lesson 2 measurement. Instead of counting cross-team dependencies plainly, we'll ask for each module: how many teams have to be in the room to change it? A module forces its owners plus the owners of everything it touches (its dependencies) to coordinate. If k teams must be in the room, the number of pairs that can disagree —the friction— is C(k,2) = k(k-1)/2, the same coordination formula from lesson 4, applied to "how many teams must synchronize over this module".

And we model the reality lesson 2 simplified: checkout isn't plainly orders' —it turned out to be co-owned by orders and payments, because both squads get their hands into it constantly—. That's the crossed module, and the metric will give it away.

# The friction of a module that crosses two teams, measured.
# Metric: pairs of teams that must coordinate to change a module.
from itertools import combinations

# Real ownership: checkout turned out to be co-owned by orders AND payments
# (both squads get their hands into it all the time). That's the crossed module.
owners = {
    "product_catalog":    {"catalog"},
    "search":             {"catalog"},
    "cart":               {"orders"},
    "order_processing":   {"orders"},
    "checkout":           {"orders", "payments"},   # <-- crosses two teams
    "payment_processing": {"payments"},
    "invoicing":          {"payments"},
    "shipping_labels":    {"shipping"},
    "delivery_tracking":  {"shipping"},
    "auth":               {"platform"},
    "notifications":      {"platform"},
}

deps = {
    "search":             ["product_catalog"],
    "cart":               ["product_catalog"],
    "checkout":           ["cart", "payment_processing", "shipping_labels",
                           "notifications", "order_processing"],
    "order_processing":   ["shipping_labels", "auth"],
    "payment_processing": ["invoicing", "auth"],
    "invoicing":          ["notifications"],
    "shipping_labels":    ["delivery_tracking"],
}

def teams_in_room(module):
    """Teams that must be in the room to change this module:
    its owners, plus the owners of everything it touches."""
    room = set(owners[module])
    for d in deps.get(module, []):
        room |= owners[d]
    return room

def coordination_pairs(module):
    """Pairs of teams that must coordinate = C(k, 2)."""
    return len(list(combinations(teams_in_room(module), 2)))

print(f"{'module':<22}{'#owners':>8}{'teams_in_room':>15}{'coord_pairs':>13}")
total = 0
for m in owners:
    k = len(teams_in_room(m))
    pairs = coordination_pairs(m)
    total += pairs
    flag = "  <-- crosses teams" if len(owners[m]) > 1 else ""
    print(f"{m:<22}{len(owners[m]):>8}{k:>15}{pairs:>13}{flag}")

print()
print(f"total system friction (sum of coord_pairs): {total}")
ck = coordination_pairs("checkout")
print(f"checkout alone contributes {ck} of {total} pairs "
      f"({ck/total*100:.0f}% of the friction) by crossing {len(teams_in_room('checkout'))} teams.")

What to expect. Running it:

module                 #owners  teams_in_room  coord_pairs
product_catalog              1              1            0
search                       1              1            0
cart                         1              2            1
order_processing             1              3            3
checkout                     2              4            6  <-- crosses teams
payment_processing           1              2            1
invoicing                    1              2            1
shipping_labels              1              1            0
delivery_tracking            1              1            0
auth                         1              1            0
notifications                1              1            0

total system friction (sum of coord_pairs): 12
checkout alone contributes 6 of 12 pairs (50% of the friction) by crossing 4 teams.

Stop at the table, because it points at the culprit with surgical precision.

Checkout is the four-restaurant kitchen. Its teams_in_room is 4 —orders and payments own it, and it touches modules of shipping (shipping_labels) and platform (notifications)—, so there are C(4,2) = 6 pairs of teams that can disagree every time someone wants to change it. Six simultaneous negotiations to touch a single module. It's, by far, the highest number in the table —second place, order_processing, has 3—. Checkout isn't just another module: it's the point where the whole system gets stuck.

A single module concentrates half of all the friction. The total friction of the system —the sum of all the coord_pairs— is 12. Checkout contributes 6 of those 12: 50%. A single module, of eleven, is responsible for half of all the expensive coordination in the system. This is what makes the diagnosis so valuable: not all modules hurt equally. If you had to fix a single thing in Mercado to reduce the friction, there'd be no doubt which —checkout, measured, shouts its name—. The other ten modules, together, contribute the other half; checkout alone, the first.

Most modules don't hurt at all. Notice the rows with coord_pairs = 0: product_catalog, search, shipping_labels, delivery_tracking, auth, notifications. They're modules a single team owns and that only touch things of their own team (or nothing). Their friction is zero —changing them requires no inter-team negotiation—. This is important because it dismantles the idea that "the whole system is a coordination disaster". It isn't: most modules are well aligned and cheap to change. The problem is localized in a few crossed modules —mainly checkout, secondarily order_processing—. The friction isn't scattered all over; it's concentrated, and that's why it's attackable.

Here's the lesson made into a number: friction is a property of the structure, and it's concentrated in the modules that cross boundaries. Checkout hurts not because its team is bad, but because its geometry —two owners, four teams in the room— generates six coordination pairs per change. No level of talent or good will lowers that 6; the only thing that lowers it is changing the geometry —giving it a single owner and clean boundaries—, which is exactly what we'll measure in lesson 6. The diagnosis tells you where to operate: not on the whole system, but on checkout, with precision.

An honest nuance about the metric. coordination_pairs counts the teams that must be in the room treating all the dependencies as if they required co-change —as if every time you touch checkout you had to renegotiate with shipping and platform—. In reality, some of those dependencies could be toward stable services (for example, notifications as a platform service with a contract that barely changes), in which case platform wouldn't have to be in the room for most changes. That's why checkout's 6 is a ceiling of its friction, not an immutable constant —and one of the cures (making notifications a stable service) lowers that ceiling without moving checkout from its team—. Lesson 6 exploits exactly that: part of the friction is resolved with a single owner, and part with turning dependencies into services. The metric gives you the map of where the pain is; the tools of lesson 6 lower it by two paths.

Deep dive: why friction isn't anyone's fault (and what an "ownerless" module is)

The most important consequence of this measurement is a mindset change: stop looking for culprits and start looking for geometries. When an area of the system is slow and fragile, the typical organizational reaction is personal —"that team doesn't deliver", "the lead should be changed", "they need more discipline"—. The measurement shows it's almost always unfair: if a module forces four teams to coordinate, it'll be slow with any team, because the problem is the module, not the people. It's the difference between blaming the drivers for the crashes at a dangerous intersection and redesigning the intersection. Good architects redesign the intersection.

This connects with a specific anti-pattern Team Topologies names: the module without a clear owner, or worse, with several owners. A module must have one responsible team —one that decides, evolves, and answers for it—. When a module has two owners (like checkout, orders and payments), the worst of two worlds happens: neither really owns it (the responsibility dilutes: "I thought the other team was taking care of it"), and everyone has to coordinate to change it (the friction multiplies). The shared module is no one's land and everyone's field at once. Bugs go unfixed because each team assumes it's the other's; changes get stuck because they require the consensus of two teams with different priorities. The measurement captures it in the #owners column: any module with #owners > 1 is a red flag, and checkout has it.

There's an even more treacherous variant: the module that officially has one owner (on paper, "checkout is orders'") but that in practice several teams modify because they need to. The org chart says there's one owner; the reality of git blame says there are four. This is the most dangerous case because the friction exists but is hidden —the diagram reassures you while the system gets stuck—. The only way to detect it is to look at the real communication and changes, not the declared ownership: who actually touches this module?, who has to approve a change?, how many teams have to be told? If the answer is "several", you have a crossed module even if the paper says it has one owner. That's why in the example we modeled checkout as co-owned by orders and payments even though in lesson 2 we listed it as "orders'": lesson 2 used the declared ownership; this one uses the real one, and the real one is where the friction lives.

The golden rule that comes out of all this, and that lessons 6 and 7 turn into a method: each module, one owner; each team, boundaries it can change without asking permission. A module with a single owner and dependencies toward stable services has zero friction —you change it without gathering anyone—. A module with several owners or with dependencies toward things that change at once has high friction —every change is a summit of teams—. The architect's work, measured, is to move the system from the second state to the first: identify the crossed modules (this lesson) and reassign owners and boundaries so they stop crossing (the following ones).

Common mistakes

Blaming the people for the friction of the structure (of attribution). What happens: a crossed module is slow, and the organization concludes the team is bad, changes the lead, applies pressure —and nothing improves, because the problem was never the team—. Why it happens: it's easier and more natural to blame visible people than an invisible geometry of dependencies. How to spot it: if an area hurts no matter who runs it —you changed the team and it's still the same—, the problem is structural, not personal. How to fix it: measure the module's friction (the coord_pairs); if it's high, the cure is to change the geometry (single owner, clean boundaries), not the people.

Leaving modules with several owners (of diluted responsibility). What happens: an important module is touched by two or three teams "because everyone needs it", and no one really owns it —bugs go orphaned, every change is a negotiation—. Why it happens: it seems efficient that "whoever needs it modifies it", but that dilutes the responsibility and multiplies the coordination. How to spot it: #owners > 1 in the measurement, or in real life, a module where you have to tell several teams to change something. How to fix it: assign one single owner and have the others consume the module through a stable interface (not getting their hands into it); the responsibility concentrates and the friction drops.

Trusting the declared ownership and not the real one (of the diagram that lies). What happens: the diagram says checkout is orders', so the analysis treats it as a single-owner module and concludes there's no problem —while in practice payments, shipping, and platform modify it and the friction is real but invisible—. Why it happens: the declared ownership is comfortable and written down; the real one has to be investigated. How to spot it: compare the declared owner with who really makes commits and approves changes (the git blame, the meetings); if they diverge, the declared one lies. How to fix it: measure with the real ownership —who actually touches the module—, as we did modeling the co-owned checkout; the friction lives in the reality, not on paper.

Exercises

Exercise 1 — Compute the friction of a new module. Mercado wants to add a gift_cards module that will depend on payment_processing (to charge the card), checkout (to apply it in the purchase), and notifications (to notify the recipient). If gift_cards will be owned by the payments squad, how many teams would be in the room to change it and how many coordination pairs would it have? Use the example's real ownership (checkout is orders + payments').

See solution

teams_in_room(gift_cards) = owners of gift_cards ∪ owners of its dependencies:

  • owner of gift_cards: payments
  • dependency payment_processing → owner payments
  • dependency checkout → owners orders, payments (co-owned)
  • dependency notifications → owner platform

Union: {payments, orders, platform} → k = 3 teams in the room.

Coordination pairs: C(3,2) = 3.

The lesson: even though gift_cards is owned by a single team (payments), depending on checkout contaminates it —since checkout is orders + payments', any module that depends on it drags orders into the room—. The friction propagates: a crossed module isn't just expensive itself, it makes everything that depends on it more expensive too. This reinforces why it's worth fixing checkout first: reducing its friction also lowers that of everything that touches it. If checkout had a single owner (say, a checkout team), then teams_in_room(gift_cards) would be {payments, checkout, platform} —still 3—, but the coordination with checkout would be through a stable interface, not a co-change; the raw number is similar, but the nature of the coordination (service vs. shared kitchen) is what really changes the pain.

Exercise 2 — The module with a ghost owner. Mercado's official diagram says order_processing is 100% orders', no problem. But you investigate and discover that every time shipping changes the label format, someone from shipping has to put a change in order_processing to adapt it. What does this tell you about the real ownership of order_processing? How would its measured friction change?

See solution

That shipping has to put changes into order_processing means the real ownership of order_processing isn't just orders': in practice, shipping also modifies it. The diagram says one owner; the git blame says two. It's the mistake of "trusting the declared ownership": the friction exists but is hidden behind a reassuring diagram.

How the measured friction changes: in the example, order_processing had owners = {orders} and teams_in_room = 3 (orders + shipping by depending on shipping_labels + platform by depending on auth), with coord_pairs = 3. If we recognize the real ownership owners = {orders, shipping}, the teams_in_room is still {orders, shipping, platform} = 3 (shipping was already there by the dependency), so the coord_pairs don't change (still 3). But something qualitative and crucial changes: order_processing now has #owners = 2, a red flag of diluted responsibility that was hidden before. Now we know order_processing is a second crossed module (after checkout), not a clean orders module.

The lesson: measuring with the real ownership reveals crossed modules the diagram hides. The cure would be either to give order_processing a real single owner (and have shipping not get its hands directly into it), or —better— have shipping expose the label format as a stable contract, so order_processing consumes it without shipping having to edit it. It's, again, the lesson 6 dilemma: single owner or stable service.

Exercise 3 — Rank the cures by impact. With the example's table, an architect has time to fix the friction of a single module this quarter. Three candidates are proposed: (a) checkout, (b) order_processing, (c) product_catalog. Rank them by impact on the total friction of the system and justify which to choose.

See solution

We look at each one's contribution to the total friction (12 pairs):

  • (a) checkout: 6 pairs (50% of the total friction). Fixing it —giving it a single owner and clean boundaries— could lower its contribution from 6 to ~1-3, cutting up to ~40% of the system's total friction in a single move.
  • (b) order_processing: 3 pairs (25%). Fixing it helps, but cuts at most a quarter of the friction.
  • (c) product_catalog: 0 pairs (0%). It already has zero friction —a single owner, internal dependencies—. Fixing it would cut nothing, because there's nothing to cut.

Order by impact: checkout (6) > order_processing (3) > product_catalog (0).

Which to choose: checkout, without a doubt. It's the one that concentrates half of all the friction in the system; with the limited time of a quarter, attacking checkout gives the highest return per unit of effort. Choosing product_catalog would be wasting the quarter on something that already works (the mistake of "optimizing what doesn't hurt"). The measurement doesn't just diagnose that there's a problem —it prioritizes where to invest the scarce effort—, which is exactly what an architect needs when they can't fix everything at once. Lesson 6 will take exactly this candidate (checkout) and measure how much the total friction drops when the inverse maneuver is applied to it.

Summary and next step

In this lesson you learned to point out, with a number, the module that most slows a system: the one that crosses team boundaries. With the kitchen shared by four restaurants you saw that a space with several owners is slow not because of the chefs but because of the geometry —four owners are six pairs to negotiate per change—. And you measured it by executing: the friction per module (C(k,2) over the teams that must be in the room) showed that Mercado's checkout, co-owned by orders and payments and touching modules of shipping and platform, crosses four teams and contributes 6 of the system's 12 friction pairs —50%, a single module—, while most modules have zero friction. You understood that friction is a property of the structure, not of the people; that a module with several owners (or with a declared owner but several real ones) is a red flag; and that the measurement doesn't just diagnose but prioritizes where to invest the scarce effort.

Before moving on you should be able to: measure a module's friction by counting the teams that must coordinate to change it; identify the crossed module that concentrates a system's friction; distinguish declared ownership from real ownership and why the real one is where the friction lives; and prioritize cures by impact on the total friction.

What follows is the cure. You have the diagnosis —checkout concentrates half of the friction by crossing four teams—; now you'll fix it, and in the only way that really works. In lesson 6 you'll apply the inverse Conway maneuver: instead of redesigning the checkout code and praying it stays separate (which lesson 1 taught us erodes), you'll change the organization —create a team that owns the entire checkout flow and turn the platform into a service— and measure the total friction collapse. It's the module's master move: not fighting Conway by redesigning the code, but using Conway in your favor by redesigning the teams to obtain the architecture you want.

Resources