Go a Escala: Patrones de Ingeniería desde Uber, Stripe y Grandes Equipos

Go a Escala: Patrones de Ingeniería desde Uber, Stripe y Grandes Equipos

Aprende cómo grandes equipos de ingeniería organizan bases de código Go para escala. Descubre la estrategia monorepo de Uber, gestión de dependencias, patrones de testing y flujos de trabajo.

Por Omar Flores

El problema que solo los grandes equipos enfrentan

Imagina que eres un desarrollador junior en una startup. Tienen 10 desarrolladores. Cada mañana alguien pushea código. Todos lo pullan. Las cosas funcionan o se rompen inmediatamente. Arreglar o revertir. Simple.

Ahora imagina que estás en Uber. Tienes 5,000 desarrolladores. Diez plataformas. Miles de servicios. Millones de líneas de código. Los cambios de código suceden miles de veces por día en toda la base de código. La mayoría de cambios tienen éxito. Algunos rompen sistemas que sirven a cientos de millones de usuarios.

La pregunta cambia de “¿funciona el código?” a “¿cómo prevenimos desastres cuando miles de personas tocan la misma base de código diariamente?”

Los ingenieros de Uber resolvieron este problema no con mejores herramientas, sino con mejor estructura. Construyeron sistemas alrededor de la organización, la propiedad y la verificación — no la complejidad.

Esta guía revela esos patrones. No teoría. Estrategias reales usadas por empresas construyendo a escala masiva.


Parte 1: El Monorepo — La base de Uber

Uber no gestiona cientos de repositorios Git separados. Uber usa un monorepo — un único repositorio Git que contiene código para virtualmente cada servicio, librería y herramienta.

Esto suena caótico. Es en realidad lo opuesto.

Por qué un monorepo tiene sentido a escala

Problema a pequeña escala: Cada servicio en su propio repo funciona bien. Pero cuando tienes 500 servicios que dependen unos de otros, gestionar versiones se vuelve imposible. Cada cambio en una librería compartida fuerza actualizaciones coordinadas en docenas de repositorios. Se convierte en un juego de infierno de dependencias.

Solución: Un repositorio. Base de código entera visible. Un cambio en una librería compartida es inmediatamente visible a todo código que la usa. Las dependencias están actualizadas automáticamente. Sin conflictos de versiones porque no hay versionado — todo está en el commit más reciente.

uber-monorepo/
  services/
    auth-service/
    payment-service/
    ride-matching/
    user-platform/
    driver-platform/
  libraries/
    common/
      errors/
      logging/
      metrics/
    storage/
      postgres/
      redis/
      cassandra/
  tools/
    deploy/
    monitoring/
    testing/
  vendor/
    dependencies/

Ventajas a escala:

  • Commits atómicos — Commiteas un cambio de librería y actualizaciones de sus consumidores al mismo tiempo. Sin estados parciales.
  • Refactoring más fácil — Renombra una función en una librería, el compilador te dice cada lugar que se rompe. Los arreglas todos en un commit.
  • Transparencia de dependencias — Ve exactamente qué depende de qué. Sin dependencias transitivas ocultas.
  • Estándares compartidos — Una configuración de lint, un estándar de test, un proceso de deployment, todos lo siguen.

Desafíos:

  • Tamaño del repositorio — El monorepo de Uber es cientos de gigabytes. Clonarlo toma tiempo. El diff es lento.
  • Sobrecarga de herramientas — Git no puede manejar decenas de miles de archivos eficientemente. Uber usa herramientas personalizadas encima de Git.
  • Complejidad de control de acceso — ¿Cómo das acceso a la Persona A al Servicio B pero no al C cuando están en el mismo repo?

La mayoría de empresas adoptan la estrategia monorepo conforme crecen más allá de 100 ingenieros y 50 servicios. Antes de eso, repos separados son más simples.

Gestionar escala de monorepo

Para monorepos grandes, Uber usa herramientas como:

Bazel (sistema de compilación)

  • Reemplaza go build
  • Entiende el grafo de dependencias completo
  • Cachea artefactos de compilación en la organización
  • Permite compilar solo código cambiado y sus dependientes

Phabricator/Arcanist (code review)

  • Entiende qué archivos cambiaron
  • Enruta reviews a equipos responsables
  • Bloquea merge si tests fallan
  • Aplica reglas de propiedad

Git con filtrado

# Solo clona el código que necesitas
git clone --filter=blob:none <monorepo-url>
git sparse-checkout set services/my-service/

Esto reduce tamaño de clone de 100GB a lo que realmente usas.


Parte 2: Propiedad y Responsabilidad — El archivo OWNERS

En un monorepo con miles de desarrolladores, no puedes tener a todos cambiando todo. Necesitas propiedad clara.

Uber usa archivos OWNERS en toda la base de código:

# services/auth-service/OWNERS
auth-team@uber.com
@mchen
@priya.patel

# services/auth-service/internal/jwt/OWNERS
@mchen (primary)
@priya.patel (backup)

Cómo funciona:

  1. Desarrollador envía un code review
  2. Sistema verifica qué archivos OWNERS aplican
  3. Esas personas se añaden automáticamente como revisores
  4. Código no puede mergearse sin su aprobación

Beneficios:

  • Claridad — Todos saben quién es dueño de qué
  • Responsabilidad — Propietario claro para cada pieza
  • Conocimiento — Sabes quién preguntar sobre un servicio
  • Compuertas de calidad — Personas experimentadas deben aprobar cambios

La regla: Cada directorio tiene un archivo OWNERS. Sin excepciones. Sin “cualquiera puede cambiar esto.”


Parte 3: Testing a escala — La pirámide de tests

Uber ejecuta millones de tests diariamente. No todos a la vez. Pero la estrategia de testing crea confianza:

La pirámide de tres niveles

        /\
       /  \
      / E2E \          Tests de integración entre servicios
     /______\         (lento, realista, pocos tests)
    /        \
   /  Integration\    Tests involucrando bases de datos reales, cachés
  /____________\      (velocidad media, cantidad media)
 /              \
/ Unit Tests    \     Tests para funciones individuales
/______________\ (rápido, muchos tests, feedback inmediato)

Nivel 1: Tests unitarios (la gran mayoría)

Rápidos. Aislados. Sin base de datos. Sin red. Todo mockeado.

// userservice/user_test.go
func TestCreateUserValidation(t *testing.T) {
	user, err := CreateUser("", "invalid")
	assert.Error(t, err)
	assert.Nil(t, user)
}

Los desarrolladores ejecutan tests unitarios localmente antes de pushear. Toma segundos. Detecta el 80% de bugs inmediatamente.

Nivel 2: Tests de integración

Base de datos real (base de datos de test). Servicios reales hablando entre sí. Más lento. Menos cantidad.

// auth_test/integration_test.go
func TestAuthServiceWithRealDatabase(t *testing.T) {
	db := setupTestDB()
	defer db.Close()

	svc := auth.NewService(db)
	token, err := svc.GenerateToken(userID)
	assert.NoError(t, err)

	valid := svc.ValidateToken(token)
	assert.True(t, valid)
}

Estos ejecutan en CI después de que los tests unitarios pasan. Toma minutos. Detecta problemas de integración.

Nivel 3: Tests E2E

Servicios reales desplegados en ambiente de test. Llamadas HTTP reales. Extremadamente lento y costoso.

// Minimal E2E tests
// Solo test de viajes de usuario críticos
// - Usuario signup → verificación → login
// - Rider reserva ride → ve conductor → obtiene rating

Solo caminos críticos. Quizás 50 tests E2E para plataforma completa. Ejecutan en cada release, no en cada commit.

Estrategia de ejecución de tests

Commit pusheado

Ejecutar unit tests (30 seg)

    ├─ PASS → Ejecutar tests de integración en paralelo (5 min)
    └─ FAIL → Notificar desarrollador, detener

    ├─ PASS → Ejecutar tests E2E en staging (30 min)
    └─ FAIL → Notificar desarrollador, detener

    ├─ PASS → Mergear a main, trigger deployment
    └─ FAIL → Bloquear merge, investigar

Principio clave: Feedback rápido para casos comunes, verificación exhaustiva antes de producción.


Parte 4: Organización de código — La estructura del servicio

En Uber, cada servicio sigue una estructura consistente. Esta uniformidad permite a cualquier desarrollador navegar cualquier servicio.

services/
  payment/
    cmd/
      payment-server/
        main.go              // Entrada del servicio
    internal/
      service/
        payment_service.go   // Lógica de negocio central
        refund_service.go
      repository/
        payment_repo.go      // Capa de base de datos
      handler/
        payment_http.go      // Handlers HTTP
    api/
      v1/
        payment.proto        // Definición de API (gRPC u OpenAPI)
    config/
      config.go             // Carga de configuración
    tests/
      integration/
        payment_test.go      // Tests de integración
    Makefile               // Desarrollo local
    go.mod
    go.sum

Por qué esta estructura:

  • cmd/ — Puntos de entrada. Un servicio = un binario = un main.go
  • internal/ — Código privado. El compilador lo aplica. No puedes importar desde internal/ de otro servicio
  • api/ — Contratos de API. Humanos y máquinas leen esto
  • config/ — Configuración centralizada
  • tests/ — Estructura de tests refleja estructura de producción

La regla: Todo desarrollador puede encontrar código en cualquier servicio usando el mismo modelo mental. Sin sorpresas.


Parte 5: Gestión de dependencias a escala

En un monorepo, las dependencias son tanto una fortaleza como un peligro.

La fortaleza: Todo código es visible. Si Servicio A depende de Librería B, ves esa relación claramente.

El peligro: Dependencias circulares. Acoplamiento estrecho. Grafos de dependencias complejos.

Reglas de dependencias

Uber aplica reglas estrictas:

1. Grafo de dependencias acíclico

Los servicios pueden depender de librerías y servicios de nivel inferior. Pero nunca circular.

User Service

Auth Library

Common Library

(Nada arriba)

Payment Service

User Service  ← PERMITIDO (depende de capa inferior)

Common Library

No permitido:

User Service → Payment Service → User Service ← CICLO

Estos son detectados por el sistema de compilación. Previenen compilar.

2. Dependencias explícitas

Cada import debe estar en go.mod. Sin dependencias implícitas. El sistema de compilación lo verifica.

import (
	"uber/services/user"           // OK - explícito en go.mod
	"uber/libraries/common/errors" // OK - explícito en go.mod
	"some-external/package"        // Debe estar en go.mod
)

3. Estrategia de versionado de dependencias

En el monorepo, todo usa HEAD (el commit más reciente). Pero dependencias externas están pinned.

// go.mod
require (
	github.com/some-library v1.2.3  // Pinned
	github.com/another-lib v2.0.0   // Pinned
)

// Internas: siempre latest
import "uber/libraries/common"  // Auto-latest

Esto previene ruptura de librerías externas mientras asegura consistencia interna.


Parte 6: Deployment y Rollback

Cuando miles de desarrolladores están pusheando código, el deployment se vuelve safety-critical.

Deployments canario

En lugar de deployar a todos los servidores a la vez, Uber deployea a un pequeño porcentaje primero:

Version 2.0.0

Deployar a 1% de servidores (canario)

Monitorear por 5 minutos
    ├─ ¿Tasa de error normal? → Continuar
    ├─ ¿Tasa de error subió? → Automatic rollback
    └─ ¿Inspección manual necesaria? → Pausar para review

Deployar a 25% (aún es seguro rollback)

Monitorear por 10 minutos
    ├─ ¿Todo bien? → Continuar
    └─ ¿Problemas? → Rollback

Deployar a 100% (rollout completo)

Métrica clave: Tasa de error. Si la tasa de error aumenta más que X% entre versiones, rollback automático se dispara.

Implementación:

// En sistema de deployment
if currentErrorRate > baselineErrorRate * 1.2 {
    // 20% aumento en errores
    rollback()
    notifyOnCall()
    createIncident()
}

Ventanas de deployment

Uber no deployea durante horas pico. El deployment sucede durante ventanas de bajo tráfico cuando los problemas son más fáciles de ver y el rollback es más rápido.


Parte 7: Cultura de code review

El code review es donde la calidad se aplica. A escala de Uber, el code review debe ser:

  1. Rápido — Tiempo promedio de review bajo 2 horas
  2. Exhaustivo — Todos los tests pasan antes de review
  3. Claro — Los comentarios explican el por qué, no el qué
  4. No-bloqueante — Los revisores sugieren, no dictan

Checklist de code review

Los revisores verifican:

  • Tests cubren caso feliz y casos de error
  • Sin nueva complejidad sin justificación
  • Dependencias explícitas
  • Implicaciones de rendimiento consideradas
  • Manejo de errores correcto
  • Logging suficiente para debugging
  • Implicaciones de seguridad revisadas (si relevante)
  • Documentación actualizada

La cultura: Los desacuerdos son sobre ideas, no personas. Un desarrollador sénior deferiendo a la solución mejor de un desarrollador junior es celebrado, no visto como debilidad.

Manejando cambios grandes

Para refactorings grandes o cambios arquitectónicos:

  1. RFC (Request for Comments) — Propuesta escrita describiendo problema, solución y trade-offs
  2. Discusión — Comentarios async y sugerencias
  3. Decisión — TL hace llamada final
  4. Implementación — Commits incrementales pequeños, cada uno revieweado
  5. Rollout — Deployment canario con monitoreo

Esto previene “sorpresas” donde cambio masivo rompe suposiciones que nadie sabía que existían.


Parte 8: Flujo de trabajo para desarrolladores nuevos

Un ingeniero nuevo en Uber sigue este onboarding:

Semana 1: Setup local

# Clone monorepo (sparse checkout, minimal)
git clone --filter=blob:none --sparse <monorepo>

# Checkout solo código del equipo
git sparse-checkout set services/my-team/

# Build y test
bazel build //services/my-team/...
bazel test //services/my-team/...

Semana 2: Primer PR

  • Bug fix pequeño o mejora de docs
  • Asignado a reviewer experimentado
  • Aprende cultura de code review

Semana 3-4: Feature pequeño

  • Feature en su servicio solo
  • Comprensión: cómo funciona testing, cómo revisar código de otros

Mes 2-3: Feature entre servicios

  • Feature que abarca múltiples servicios
  • Comprensión: dependencias, coordinación, sistemas más grandes

Esta escalada gradual previene “nuevo desarrollador rompe prod” mientras construye conocimiento.


Parte 9: Lecciones para tu equipo

No necesitas escala de Uber para beneficiarte de estos patrones.

Aplicable inmediatamente:

  1. Archivos OWNERS — Incluso para 10 desarrolladores, sabe quién es dueño de qué
  2. Pirámide de tests — Tests unitarios rápidos, tests de integración exhaustivos
  3. Estructura consistente — Nuevo servicio = mismo layout de directorio
  4. Dependencias explícitas — go.mod siempre completo y preciso
  5. Estándares de code review — Checklist, async, rápido
  6. Aciclidad de dependencias — Verifica esto conforme creces

Adopta conforme escalas:

  1. Monorepo — Cuando tengas 50+ servicios
  2. Sistema de compilación — Herramientas personalizadas para caché y paralelización
  3. Automatización de propiedad — Archivos OWNERS + aplicación en CI
  4. Deployments canario — Monitoreo de tasa de error, rollback automático
  5. Rollouts escalonados — 1% → 25% → 100%

Parte 10: La verdad incómoda

La infraestructura de Uber no es mejor porque Uber tiene más ingenieros. Uber tiene mejor infraestructura por decisiones tomadas cuando eran más pequeños — decisiones que escalaron.

La mayoría de empresas abordan escala reactivamente. Tienen 100 desarrolladores trabajando en 50 servicios, cada uno en repos separados, sin propiedad clara, estructura de código inconsistente. Luego contratan 500 más y se preguntan por qué todo se rompe.

Uber construyó patrones que escalaron de 10 desarrolladores a 5,000. Esa es la perspectiva. No las herramientas. No el presupuesto. La arquitectura intencional.

Y aquí está la cosa: Puedes adoptar estos patrones hoy.

No necesitas Bazel para aplicar dependencias acíclicas. Necesitas un archivo OWNERS y alguien revisando PRs. No necesitas el sistema de deployment de Uber para hacer releases canario. Necesitas deployar a 10% primero y monitorear. No necesitas un monorepo para tener estructura de código consistente. Necesitas un template que cada servicio sigue.

El factor limitante no es herramientas. Es disciplina.

Los mejores grandes equipos de ingeniería no se distinguen por sus herramientas o su presupuesto. Se distinguen por su disciplina en mantener claridad cuando los sistemas crecen complejos. Esa disciplina, aplicada temprano, escala infinitamente.

Tags

#go #golang #ingeniería-a-escala #monorepo #flujo-trabajo #arquitectura #uber #grandes-equipos #organización-código #gestión-dependencias #testing #ci-cd #mejores-prácticas