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.
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:
- Desarrollador envía un code review
- Sistema verifica qué archivos
OWNERSaplican - Esas personas se añaden automáticamente como revisores
- 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 = unmain.gointernal/— Código privado. El compilador lo aplica. No puedes importar desde internal/ de otro servicioapi/— Contratos de API. Humanos y máquinas leen estoconfig/— Configuración centralizadatests/— 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:
- Rápido — Tiempo promedio de review bajo 2 horas
- Exhaustivo — Todos los tests pasan antes de review
- Claro — Los comentarios explican el por qué, no el qué
- 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:
- RFC (Request for Comments) — Propuesta escrita describiendo problema, solución y trade-offs
- Discusión — Comentarios async y sugerencias
- Decisión — TL hace llamada final
- Implementación — Commits incrementales pequeños, cada uno revieweado
- 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:
- Archivos OWNERS — Incluso para 10 desarrolladores, sabe quién es dueño de qué
- Pirámide de tests — Tests unitarios rápidos, tests de integración exhaustivos
- Estructura consistente — Nuevo servicio = mismo layout de directorio
- Dependencias explícitas — go.mod siempre completo y preciso
- Estándares de code review — Checklist, async, rápido
- Aciclidad de dependencias — Verifica esto conforme creces
Adopta conforme escalas:
- Monorepo — Cuando tengas 50+ servicios
- Sistema de compilación — Herramientas personalizadas para caché y paralelización
- Automatización de propiedad — Archivos OWNERS + aplicación en CI
- Deployments canario — Monitoreo de tasa de error, rollback automático
- 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
Artículos relacionados
API Versioning Strategies: Cómo Evolucionar APIs sin Romper Clientes
Una guía exhaustiva sobre estrategias de versionado de APIs: URL versioning vs Header versioning, cómo deprecar endpoints sin shock, migration patterns reales, handling de cambios backwards-incompatibles, y decisiones arquitectónicas que importan. Con 50+ ejemplos de código en Go.
Arquitecto de Software: El Arte de Comunicar Decisiones Complejas
Guía completa sobre cómo un arquitecto de software comunica efectivamente con técnicos, operativos, gerencia y negocio. De novato a experto en comunicación empresarial.
Arquitectura de Software: De 0 a Arquitecto de Sistemas Empresariales
Guía completa sobre arquitectura de software empresarial. Patrones, C4, microservicios, B2B, multi-tenant, casos reales, antipatrones y mejores prácticas. Enfocado en negocio y decisiones estratégicas.