Módulo 2: La Ley de Conway

5. Los módulos que cruzan fronteras de equipo

Descripción

Al terminar esta lección vas a poder señalar, con un número, cuál módulo de un sistema es el que más lo frena —y por qué la culpa nunca es de las personas, sino de la estructura—. El módulo culpable es el que cruza fronteras de equipo: el que dos o más squads consideran suyo y tocan todo el tiempo, de modo que cada cambio necesita a varios equipos de acuerdo. En Mercado ese módulo es el checkout: orders lo considera suyo (es el final del flujo de compra), pero payments le mete mano constantemente (el checkout es donde se cobra), y en la práctica shipping y platform también lo tocan (el checkout dispara el envío y las notificaciones). Un módulo así es el punto donde el impuesto de Conway —que la lección 4 te enseñó a ver— se concentra: cada par de equipos que debe coordinarse para cambiarlo es un canal inter-equipo del más caro, y el checkout obliga a cuatro equipos a estar en la sala a la vez. Vas a medir esa fricción con una métrica precisa —los pares de equipos que deben coordinarse para cambiar cada módulo, C(k,2)— y ver que el checkout, solo él, aporta la mitad de toda la fricción del sistema.

Esto importa porque cambia a quién y a qué culpas cuando un área del sistema es lenta y frágil. El instinto es culpar a la gente: "el equipo de checkout es desorganizado", "no se coordinan bien". La medición muestra lo contrario: la fricción no es un defecto de las personas, es una propiedad de la estructura. Pon a los mejores ingenieros del mundo a mantener un módulo que cuatro equipos poseen a la vez, y van a ser lentos —no por incapaces, sino porque cada decisión requiere una reunión de cuatro equipos con prioridades distintas—. El módulo cruzado genera fricción del mismo modo que una intersección sin semáforo genera choques: no importa qué tan buenos sean los conductores, la geometría del cruce es el problema. Y este diagnóstico es el que habilita la cura: una vez que puedes medir que el checkout concentra el 50% de la fricción por cruzar cuatro equipos, sabes exactamente dónde aplicar la maniobra inversa de la lección 6 —darle un dueño único para que su coordinación deje de ser inter-equipo y se vuelva interna—.

Conexión con el módulo: esta lección es el diagnóstico fino. La lección 2 midió el desalineamiento global (61.5% cross-equipo); la 3 explicó su origen (el monolito de un equipo repartido en cinco); la 4 dio el porqué económico (la coordinación inter-equipo es cara). Esta las junta y las apunta a un solo módulo: ¿cuál es el que más duele, y cuánto? Refina la métrica de la lección 2 —en vez de contar dependencias cross-team a secas, pesa cuántos equipos deben coordinar simultáneamente para cambiar cada módulo, que captura mejor el dolor real—. Y entrega el objetivo preciso para la cura: el número que produces aquí (checkout = 6 de 12 pares de coordinación) es el "antes" que la maniobra inversa de la lección 6 va a bajar. Si la lección 4 fue "la coordinación cross-team es cara", esta es "y aquí está el módulo que la concentra, medido".

La cocina que comparten cuatro restaurantes

Piénsalo así. En una plaza de comidas hay cuatro restaurantes: una taquería, una pizzería, una cafetería y una juguería. Por ahorrar, los cuatro comparten una sola cocina. Cada restaurante tiene su propio menú, su propio dueño, su propio ritmo. Pero todos cocinan en el mismo espacio, con las mismas estufas, el mismo refrigerador, la misma tarja.

¿Qué pasa cada vez que uno quiere cambiar algo? La taquería quiere mover la plancha para hacer más espacio: no puede decidirlo sola, porque la plancha estorba a la pizzería. La cafetería quiere reorganizar el refri: tiene que consultar a los otros tres, porque todos guardan ahí sus insumos. Comprar una estufa nueva, cambiar el horario de limpieza, reacomodar la tarja —cualquier cambio, por chico que sea, requiere que los cuatro dueños se pongan de acuerdo—. Y ponerse de acuerdo entre cuatro dueños con prioridades distintas es lento y tenso: la taquería tiene prisa, la juguería no quiere gastar, la pizzería está de acuerdo pero solo si es después del almuerzo. Una cocina compartida por cuatro no es cuatro veces más difícil de coordinar que una cocina propia; es seis veces más difícil, porque hay seis pares de dueños que pueden estar en desacuerdo (taquería-pizzería, taquería-cafetería, taquería-juguería, pizzería-cafetería, pizzería-juguería, cafetería-juguería).

Compara con el restaurante de al lado, que tiene su propia cocina. Cuando quiere mover la plancha, la mueve. Cuando quiere cambiar el refri, lo cambia. Cero reuniones, cero negociaciones. Un solo dueño decide y ejecuta. Ese restaurante es rápido no porque su chef sea mejor, sino porque nadie más comparte su cocina.

El checkout de Mercado es la cocina compartida por cuatro. Orders, payments, shipping y platform lo tocan todos, así que cualquier cambio al checkout es una reunión de cuatro equipos —seis pares que negociar—. Y como en la plaza de comidas, la lentitud no es culpa de ningún equipo en particular: es la geometría de compartir. Esta lección mide esa geometría, y muestra que la solución no es "que se coordinen mejor" (imposible mejorar seis negociaciones simultáneas) sino "darle al checkout su propia cocina" —un dueño único—, que es la maniobra inversa de la lección 6.

Ejemplo trabajado: la fricción por módulo, medida

Vamos a refinar la medición de la lección 2. En vez de contar dependencias cross-team a secas, vamos a preguntar por cada módulo: ¿cuántos equipos tienen que estar en la sala para cambiarlo? Un módulo obliga a coordinar a sus dueños más los dueños de todo lo que toca (sus dependencias). Si k equipos deben estar en la sala, el número de pares que pueden estar en desacuerdo —la fricción— es C(k,2) = k(k-1)/2, la misma fórmula de la coordinación de la lección 4, aplicada a "cuántos equipos deben sincronizarse por este módulo".

Y modelamos la realidad que la lección 2 simplificó: el checkout no es de orders a secas —resultó estar co-poseído por orders y payments, porque los dos squads le meten mano constantemente—. Ese es el módulo cruzado, y la métrica lo va a delatar.

# La friccion de un modulo que cruza dos equipos, medida.
# Metrica: pares de equipos que deben coordinarse para cambiar un modulo.
from itertools import combinations

# Ownership real: checkout resulto estar co-poseido por orders Y payments
# (los dos squads le meten mano todo el tiempo). Ese es el modulo cruzado.
owners = {
    "product_catalog":    {"catalog"},
    "search":             {"catalog"},
    "cart":               {"orders"},
    "order_processing":   {"orders"},
    "checkout":           {"orders", "payments"},   # <-- cruza dos equipos
    "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):
    """Equipos que deben estar en la sala para cambiar este modulo:
    sus duenos, mas los duenos de todo lo que toca."""
    room = set(owners[module])
    for d in deps.get(module, []):
        room |= owners[d]
    return room

def coordination_pairs(module):
    """Pares de equipos que deben coordinarse = 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 = "  <-- cruza equipos" if len(owners[m]) > 1 else ""
    print(f"{m:<22}{len(owners[m]):>8}{k:>15}{pairs:>13}{flag}")

print()
print(f"friccion total del sistema (suma de coord_pairs): {total}")
ck = coordination_pairs("checkout")
print(f"checkout solo aporta {ck} de {total} pares "
      f"({ck/total*100:.0f}% de la friccion) por cruzar {len(teams_in_room('checkout'))} equipos.")

Qué esperar. Al correrlo:

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  <-- cruza equipos
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

friccion total del sistema (suma de coord_pairs): 12
checkout solo aporta 6 de 12 pares (50% de la friccion) por cruzar 4 equipos.

Detente en la tabla, porque señala al culpable con precisión quirúrgica.

El checkout es la cocina de cuatro restaurantes. Su teams_in_room es 4 —orders y payments lo poseen, y toca módulos de shipping (shipping_labels) y platform (notifications)—, así que hay C(4,2) = 6 pares de equipos que pueden estar en desacuerdo cada vez que alguien quiere cambiarlo. Seis negociaciones simultáneas para tocar un solo módulo. Es, con diferencia, el número más alto de la tabla —el segundo lugar, order_processing, tiene 3—. El checkout no es un módulo más: es el punto donde el sistema entero se atora.

Un módulo solo concentra la mitad de toda la fricción. La fricción total del sistema —la suma de todos los coord_pairs— es 12. El checkout aporta 6 de esos 12: el 50%. Un solo módulo, de once, es responsable de la mitad de toda la coordinación cara del sistema. Esto es lo que hace tan valioso el diagnóstico: no todos los módulos duelen igual. Si tuvieras que arreglar una sola cosa en Mercado para reducir la fricción, no habría duda de cuál —el checkout, medido, grita su nombre—. Los otros diez módulos, juntos, aportan la otra mitad; el checkout solo, la primera.

La mayoría de los módulos no duele nada. Fíjate en las filas con coord_pairs = 0: product_catalog, search, shipping_labels, delivery_tracking, auth, notifications. Son módulos que un solo equipo posee y que solo tocan cosas de su propio equipo (o nada). Su fricción es cero —cambiarlos no requiere ninguna negociación inter-equipo—. Esto es importante porque desmonta la idea de que "todo el sistema es un desastre de coordinación". No lo es: la mayoría de los módulos están bien alineados y son baratos de cambiar. El problema está localizado en unos pocos módulos cruzados —principalmente el checkout, secundariamente order_processing—. La fricción no está regada por todos lados; está concentrada, y por eso es atacable.

Aquí está la lección hecha número: la fricción es una propiedad de la estructura, y está concentrada en los módulos que cruzan fronteras. El checkout duele no porque su equipo sea malo, sino porque su geometría —dos dueños, cuatro equipos en la sala— genera seis pares de coordinación por cada cambio. Ningún nivel de talento o buena voluntad baja ese 6; lo único que lo baja es cambiar la geometría —darle un dueño único y fronteras limpias—, que es exactamente lo que mediremos en la lección 6. El diagnóstico te dice dónde operar: no en todo el sistema, sino en el checkout, con precisión.

Un matiz honesto sobre la métrica. coordination_pairs cuenta los equipos que deben estar en la sala tratando todas las dependencias como si requirieran co-cambio —como si cada vez que tocas el checkout tuvieras que renegociar con shipping y platform—. En la realidad, algunas de esas dependencias podrían ser hacia servicios estables (por ejemplo, notifications como un servicio de plataforma con un contrato que casi no cambia), en cuyo caso platform no tendría que estar en la sala para la mayoría de los cambios. Por eso el 6 del checkout es un techo de su fricción, no una constante inmutable —y una de las curas (volver notifications un servicio estable) baja ese techo sin mover al checkout de equipo—. La lección 6 explota exactamente eso: parte de la fricción se resuelve con dueño único, y parte con convertir dependencias en servicios. La métrica te da el mapa de dónde está el dolor; las herramientas de la lección 6 lo bajan por dos vías.

Profundización: por qué la fricción no es culpa de nadie (y qué es un módulo "huérfano de dueño")

La consecuencia más importante de esta medición es un cambio de mentalidad: dejar de buscar culpables y empezar a buscar geometrías. Cuando un área del sistema es lenta y frágil, la reacción organizacional típica es personal —"ese equipo no entrega", "hay que cambiar al líder", "necesitan más disciplina"—. La medición muestra que casi siempre es injusto: si un módulo obliga a cuatro equipos a coordinar, va a ser lento con cualquier equipo, porque el problema es el módulo, no las personas. Es la diferencia entre culpar a los conductores por los choques en una intersección peligrosa y rediseñar la intersección. Los buenos arquitectos rediseñan la intersección.

Esto conecta con un anti-patrón específico que Team Topologies nombra: el módulo sin un dueño claro, o peor, con varios dueños. Un módulo debe tener un equipo responsable —uno que decide, evoluciona y responde por él—. Cuando un módulo tiene dos dueños (como el checkout, orders y payments), pasa lo peor de dos mundos: ninguno lo posee de verdad (la responsabilidad se diluye: "pensé que lo cuidaba el otro equipo"), y todos tienen que coordinar para cambiarlo (la fricción se multiplica). El módulo compartido es tierra de nadie y campo de todos a la vez. Los bugs se quedan sin arreglar porque cada equipo asume que es del otro; los cambios se atoran porque requieren consenso de dos equipos con prioridades distintas. La medición lo captura en la columna #owners: cualquier módulo con #owners > 1 es una bandera roja, y el checkout la tiene.

Hay una variante todavía más traicionera: el módulo que oficialmente tiene un dueño (en el papel, "el checkout es de orders") pero que en la práctica varios equipos modifican porque lo necesitan. El organigrama dice que hay un dueño; la realidad del git blame dice que hay cuatro. Este es el caso más peligroso porque la fricción existe pero está oculta —el diagrama te tranquiliza mientras el sistema se atora—. La única forma de detectarlo es mirar la comunicación y los cambios reales, no el ownership declarado: ¿quién de verdad toca este módulo?, ¿quién tiene que aprobar un cambio?, ¿a cuántos equipos hay que avisar? Si la respuesta es "varios", tienes un módulo cruzado aunque el papel diga que tiene un dueño. Por eso en el ejemplo modelamos el checkout como co-poseído por orders y payments aunque en la lección 2 lo pusimos como "de orders": la lección 2 usó el ownership declarado; esta usa el real, y el real es donde vive la fricción.

La regla de oro que sale de todo esto, y que las lecciones 6 y 7 convierten en método: cada módulo, un dueño; cada equipo, fronteras que puede cambiar sin pedir permiso. Un módulo con dueño único y dependencias hacia servicios estables tiene fricción cero —lo cambias sin reunir a nadie—. Un módulo con varios dueños o con dependencias hacia cosas que cambian a la vez tiene fricción alta —cada cambio es una cumbre de equipos—. El trabajo del arquitecto, medido, es mover el sistema del segundo estado al primero: identificar los módulos cruzados (esta lección) y reasignar dueños y fronteras para que dejen de cruzar (las siguientes).

Errores comunes

Culpar a las personas por la fricción de la estructura (de atribución). Qué pasa: un módulo cruzado es lento, y la organización concluye que el equipo es malo, cambia al líder, mete presión —y nada mejora, porque el problema nunca fue el equipo—. Por qué pasa: es más fácil y más natural culpar a personas visibles que a una geometría invisible de dependencias. Cómo detectarlo: si un área duele sin importar quién la maneje —cambiaste al equipo y sigue igual—, el problema es estructural, no personal. Cómo corregirlo: mide la fricción del módulo (los coord_pairs); si es alta, la cura es cambiar la geometría (dueño único, fronteras limpias), no las personas.

Dejar módulos con varios dueños (de responsabilidad diluida). Qué pasa: un módulo importante lo tocan dos o tres equipos "porque todos lo necesitan", y nadie lo posee de verdad —los bugs se quedan huérfanos, cada cambio es una negociación—. Por qué pasa: parece eficiente que "quien lo necesite lo modifique", pero eso diluye la responsabilidad y multiplica la coordinación. Cómo detectarlo: #owners > 1 en la medición, o en la vida real, un módulo donde tienes que avisarle a varios equipos para cambiar algo. Cómo corregirlo: asigna un dueño único y haz que los demás consuman el módulo a través de una interfaz estable (no metiéndole mano); la responsabilidad se concentra y la fricción cae.

Fiarse del ownership declarado y no del real (de diagrama que miente). Qué pasa: el diagrama dice que el checkout es de orders, así que el análisis lo trata como un módulo de un solo dueño y concluye que no hay problema —mientras en la práctica payments, shipping y platform lo modifican y la fricción es real pero invisible—. Por qué pasa: el ownership declarado es cómodo y está escrito; el real hay que investigarlo. Cómo detectarlo: compara el dueño declarado con quién realmente hace commits y aprueba cambios (el git blame, las reuniones); si divergen, el declarado miente. Cómo corregirlo: mide con el ownership real —quién de verdad toca el módulo—, como hicimos al modelar el checkout co-poseído; la fricción vive en la realidad, no en el papel.

Ejercicios

Ejercicio 1 — Calcula la fricción de un módulo nuevo. Mercado quiere agregar un módulo gift_cards (tarjetas de regalo) que dependerá de payment_processing (para cobrar la tarjeta), checkout (para aplicarla en la compra) y notifications (para avisar al destinatario). Si gift_cards lo va a poseer el squad payments, ¿cuántos equipos estarían en la sala para cambiarlo y cuántos pares de coordinación tendría? Usa el ownership real del ejemplo (checkout es de orders+payments).

Ver solución

teams_in_room(gift_cards) = dueños de gift_cards ∪ dueños de sus dependencias:

  • dueño de gift_cards: payments
  • dependencia payment_processing → dueño payments
  • dependencia checkout → dueños orders, payments (co-poseído)
  • dependencia notifications → dueño platform

Unión: {payments, orders, platform} → k = 3 equipos en la sala.

Pares de coordinación: C(3,2) = 3.

La lección: aunque gift_cards lo posea un solo equipo (payments), depender del checkout lo contamina —como el checkout es de orders+payments, cualquier módulo que dependa de él arrastra a orders a la sala—. La fricción se propaga: un módulo cruzado no solo es caro él mismo, sino que encarece a todos los que dependen de él. Esto refuerza por qué conviene arreglar el checkout primero: reducir su fricción baja también la de todo lo que lo toca. Si el checkout tuviera un dueño único (digamos, un equipo checkout), entonces teams_in_room(gift_cards) sería {payments, checkout, platform} —igual 3—, pero la coordinación con checkout sería a través de una interfaz estable, no un co-cambio; el número bruto es parecido, pero la naturaleza de la coordinación (servicio vs cocina compartida) es lo que de verdad cambia el dolor.

Ejercicio 2 — El módulo con dueño fantasma. El diagrama oficial de Mercado dice que order_processing es 100% de orders, sin problema. Pero investigas y descubres que cada vez que shipping cambia el formato de las etiquetas, alguien de shipping tiene que meter un cambio en order_processing para adaptarlo. ¿Qué te dice esto sobre el ownership real de order_processing? ¿Cómo cambiaría su fricción medida?

Ver solución

Que shipping tenga que meter cambios en order_processing significa que el ownership real de order_processing no es solo orders: en la práctica, shipping también lo modifica. El diagrama dice un dueño; el git blame dice dos. Es el error de "fiarse del ownership declarado": la fricción existe pero está oculta detrás de un diagrama tranquilizador.

Cómo cambia la fricción medida: en el ejemplo, order_processing tenía owners = {orders} y teams_in_room = 3 (orders + shipping por depender de shipping_labels + platform por depender de auth), con coord_pairs = 3. Si reconocemos el ownership real owners = {orders, shipping}, el teams_in_room sigue siendo {orders, shipping, platform} = 3 (shipping ya estaba por la dependencia), así que los coord_pairs no cambian (siguen en 3). Pero cambia algo cualitativo crucial: order_processing pasa a tener #owners = 2, una bandera roja de responsabilidad diluida que antes estaba oculta. Ahora sabemos que order_processing es un segundo módulo cruzado (después del checkout), no un módulo limpio de orders.

La lección: medir con el ownership real revela módulos cruzados que el diagrama oculta. La cura sería o darle a order_processing un dueño único de verdad (y que shipping no le meta mano directo), o —mejor— hacer que shipping exponga el formato de etiquetas como un contrato estable, para que order_processing lo consuma sin que shipping tenga que editarlo. Es, otra vez, la disyuntiva de la lección 6: dueño único o servicio estable.

Ejercicio 3 — Ordena las curas por impacto. Con la tabla del ejemplo, un arquitecto tiene tiempo para arreglar la fricción de un solo módulo este trimestre. Tres candidatos se proponen: (a) el checkout, (b) order_processing, (c) product_catalog. Ordénalos por impacto en la fricción total del sistema y justifica cuál elegir.

Ver solución

Miramos la contribución de cada uno a la fricción total (12 pares):

  • (a) checkout: 6 pares (50% de la fricción total). Arreglarlo —darle dueño único y fronteras limpias— podría bajar su contribución de 6 a ~1-3, recortando hasta un ~40% de la fricción total del sistema de un solo golpe.
  • (b) order_processing: 3 pares (25%). Arreglarlo ayuda, pero recorta como mucho un cuarto de la fricción.
  • (c) product_catalog: 0 pares (0%). Ya tiene fricción cero —un solo dueño, dependencias internas—. Arreglarlo no recortaría nada, porque no hay nada que recortar.

Orden por impacto: checkout (6) > order_processing (3) > product_catalog (0).

Cuál elegir: el checkout, sin duda. Es el que concentra la mitad de toda la fricción del sistema; con el tiempo limitado de un trimestre, atacar el checkout da el mayor retorno por unidad de esfuerzo. Elegir product_catalog sería malgastar el trimestre en algo que ya funciona (el error de "optimizar lo que no duele"). La medición no solo diagnostica que hay un problema —te prioriza dónde invertir el esfuerzo escaso—, que es justo lo que un arquitecto necesita cuando no puede arreglar todo a la vez. La lección 6 va a tomar exactamente este candidato (el checkout) y medir cuánto baja la fricción total al aplicarle la maniobra inversa.

Resumen y siguiente paso

En esta lección aprendiste a señalar, con un número, el módulo que más frena un sistema: el que cruza fronteras de equipo. Con la cocina compartida por cuatro restaurantes viste que un espacio con varios dueños es lento no por los chefs sino por la geometría —cuatro dueños son seis pares que negociar por cada cambio—. Y lo mediste ejecutando: la fricción por módulo (C(k,2) sobre los equipos que deben estar en la sala) mostró que el checkout de Mercado, co-poseído por orders y payments y tocando módulos de shipping y platform, cruza cuatro equipos y aporta 6 de los 12 pares de fricción del sistema —el 50%, un solo módulo—, mientras la mayoría de los módulos tiene fricción cero. Entendiste que la fricción es una propiedad de la estructura, no de las personas; que un módulo con varios dueños (o con dueño declarado pero varios reales) es una bandera roja; y que la medición no solo diagnostica sino que prioriza dónde invertir el esfuerzo escaso.

Antes de avanzar deberías poder: medir la fricción de un módulo contando los equipos que deben coordinarse para cambiarlo; identificar el módulo cruzado que concentra la fricción de un sistema; distinguir el ownership declarado del real y por qué el real es donde vive la fricción; y priorizar curas por impacto en la fricción total.

Lo que sigue es la cura. Tienes el diagnóstico —el checkout concentra la mitad de la fricción por cruzar cuatro equipos—; ahora vas a arreglarlo, y de la única forma que funciona de verdad. En la lección 6 vas a aplicar la maniobra inversa de Conway: en vez de rediseñar el código del checkout y rezar para que se mantenga separado (que la lección 1 nos enseñó que se erosiona), vas a cambiar la organización —crear un equipo que posea el flujo de checkout completo y volver la plataforma un servicio— y medir la fricción total desplomarse. Es la jugada maestra del módulo: no pelear contra Conway rediseñando el código, sino usar Conway a tu favor rediseñando los equipos para obtener la arquitectura que quieres.

Recursos