07 — Ingeniería de Software
Del C cercano al silicio al TypeScript en el edge
Elijo el lenguaje según el modo de fallo que intento evitar.
Rust donde la concurrencia debe ser demostrablemente correcta, Go donde los servicios de red tienen que mantenerse simples, C cerca del hardware, Python y TypeScript donde los modelos y las personas se encuentran con el sistema — elegidos con deliberación, operados en profundidad.
La medida honesta de un ingeniero no es el stack de un currículum — es lo que ha sobrevivido al contacto con usuarios reales. La mía son once años de eso.
Durante más de once años he entregado aplicaciones móviles a usuarios reales en iOS, Android y Windows. Ese número es la credencial en la que más confío, porque entregar a un dispositivo en el bolsillo de alguien es implacable: no hay entorno de staging para un crash en el teléfono de un desconocido, ni rollback para una función que confunde a quien la usa. Una década de eso enseña una disciplina específica — fragmentación de versiones, comportamiento offline, presupuestos de batería y memoria, y la lenta acumulación de criterio sobre qué dejar fuera.
Detrás de cada una de esas apps había un backend y la infraestructura en la nube debajo. Así que la práctica no es solo el frente del vidrio; va desde el evento táctil hacia abajo, a través de la API, la cola, el almacén de datos y el contenedor, hasta el kernel sobre el que corre el contenedor. He pasado suficiente tiempo en cada capa para saber dónde están las costuras, y para diseñar de modo que las costuras no se desgarren bajo carga.
Lo que he sumado desde entonces es profundidad en lenguajes de sistemas y entrega multinube: Rust y Go para los núcleos que tienen que ser correctos y rápidos, Docker y Kubernetes para la entrega, y una negativa deliberada a atar nada de ello a un solo proveedor. El resto de esta página es la forma específica de eso — lenguajes, plataformas, nubes y las pequeñas herramientas que construyo para mí por el camino.
años entregando aplicaciones móviles a usuarios reales
plataformas móviles en producción: iOS, Android, Windows
nubes operadas sin atadura a un proveedor: GCP, Azure, AWS
lenguajes principales dominados en profundidad: Rust, Go, Python, TypeScript
Un stack elegido por capa, no por moda.
Diagrama de capas de lenguajes de sistemas
Cada lenguaje se gana su lugar en la capa donde sus garantías importan.
No recurro a un lenguaje favorito y tuerzo cada problema hacia él. El diagrama se lee desde el silicio hacia arriba: C y C++ donde el código toca el hardware, Rust para los núcleos concurrentes y seguros en memoria, Go para la capa de red concurrente, luego Python y TypeScript en la superficie donde viven la IA, los datos y los usuarios.
Los anchos de las barras son un sentido aproximado de la amplitud de uso a través de mi trabajo — no un benchmark, solo dónde tiende a asentarse el volumen de código.
- Las capas inferiores compran determinismo y seguridad
- Las capas superiores compran velocidad y alcance
- La frontera es una elección deliberada, hecha por proyecto
Cómo se ve realmente el código.
Un pool acotado de workers en Go — repartir el trabajo entre goroutines, recoger resultados a través de un channel y respetar la cancelación. El patrón aparece bajo casi todos los servicios de red que escribo.
// FanOut runs fn across at most n goroutines, streaming results
// back on a channel and stopping early if ctx is cancelled.
func FanOut[T, R any](ctx context.Context, in []T, n int, fn func(T) R) []R {
jobs := make(chan int)
out := make([]R, len(in))
var wg sync.WaitGroup
for w := 0; w < n; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := range jobs {
select {
case <-ctx.Done():
return
default:
out[i] = fn(in[i])
}
}
}()
}
for i := range in {
select {
case <-ctx.Done():
close(jobs)
wg.Wait()
return out
case jobs <- i:
}
}
close(jobs)
wg.Wait()
return out
} A dónde va realmente la ingeniería.
Elegir el lenguaje según el modo de fallo que estoy evitando.
Rust es donde la corrección bajo concurrencia es innegociable. El borrow checker no es un impuesto — es una prueba en tiempo de compilación de que una clase de carreras de datos y de use-after-free no puede ocurrir, que es exactamente lo que quiero para un núcleo crítico de rendimiento que tiene que correr desatendido.
Go es donde quiero servicios de red concurrentes que pueda razonar con rapidez: goroutines y channels para el modelo de concurrencia, el profiler pprof integrado para encontrar la ruta caliente, y un binario estático que se despliega limpio dentro de un contenedor. C y C++ quedan reservados para el trabajo cercano al hardware — firmware, SoC embebido, el código que se asienta directamente sobre el mapa de registros.
- Rust — ownership, lifetimes, abstracciones de coste cero, concurrencia sin miedo
- Go — goroutines, channels, cancelación por context, profiling con pprof
- C / C++ — firmware embebido, control cercano al hardware, temporización determinista
- Bash y SQL en producción, sobre PostgreSQL y BigQuery
Once años entregando a dispositivos reales, no a simuladores.
En iOS trabajo en Swift y Objective-C con SwiftUI para superficies nuevas, Core Data para persistencia, Core Animation para el movimiento y SiriKit para intents de voz. En Android es Kotlin y Java contra el Android SDK, con la disciplina de fragmentación de versiones que viene de soportar muchas versiones de SO en el campo.
En multiplataforma recurro a React Native (avanzado) o Flutter (avanzado) cuando una base de código compartida se gana su lugar, y he entregado con Xamarin a un nivel funcional. Tras las apps se asientan backends reales — Node/Express, Spring Boot, Django/Flask, Ruby on Rails, o Go con Gin y Gorm — exponiendo REST y GraphQL.
- iOS — Swift, Objective-C, SwiftUI, Core Data, Core Animation, SiriKit
- Android — Kotlin, Java, Android SDK en muchas versiones de SO
- Multiplataforma — React Native, Flutter (avanzado), Xamarin (funcional)
- Datos — SQLite, Redis, ORMs (Sequelize, Mongoose); APIs REST y GraphQL
Portable por diseño, no casado con un solo proveedor.
Mantengo las cargas de trabajo portables para que la nube siga siendo un commodity y no una dependencia. En GCP uso Vertex AI, Cloud Run, Cloud Functions, GKE, Pub/Sub, BigQuery y Firestore. En Azure, AKS, Functions, Cognitive Services y Container Apps. En AWS, Lambda, ECS/EKS, S3, RDS y SQS/SNS detrás de CloudFront.
En el edge corro Cloudflare Workers y Vercel Edge; para productos de evolución rápida me apoyo en Supabase o Firebase como backend. Todo se entrega a través de contenedores — Docker y Kubernetes — con CI/CD en GitHub Actions o GitLab CI e infraestructura definida como código, de modo que un entorno es reproducible en lugar de construido a mano.
- GCP — Vertex AI, Cloud Run, Cloud Functions, GKE, Pub/Sub, BigQuery, Firestore
- Azure — AKS, Functions, Cognitive Services, Container Apps
- AWS — Lambda, ECS/EKS, S3, RDS, SQS/SNS, CloudFront
- Edge y BaaS — Cloudflare Workers, Vercel Edge, Supabase, Firebase
Sistemas distribuidos que se mantienen correctos mientras las cosas se mueven.
El trabajo de backend es donde el pensamiento de sistemas rinde. Diseño en torno a colas de mensajes y arquitectura dirigida por eventos para que los componentes queden desacoplados, y en torno a sistemas en tiempo real donde WebSocket y la comunicación a nivel de socket llevan el estado entre cliente y servidor con baja latencia.
Los protocolos servicio a servicio, la idempotencia y la contrapresión son preocupaciones de primer orden, no añadidos de última hora. La meta es una topología que se degrade con elegancia — que un servicio que falla ralentice el sistema, no lo detenga.
- Colas de mensajes y arquitectura dirigida por eventos
- Sistemas en tiempo real sobre WebSocket y comunicación a nivel de socket
- Protocolos servicio a servicio, idempotencia, contrapresión
- Coordinación distribuida con degradación elegante
Una aplicación, tres nubes, sin atadura.
Una carga de trabajo que mantengo portable: contenedores e infraestructura como código en el centro, desplegable a cualquier proveedor en el que un encargo ya viva. La nube es una capa commodity, no una dependencia.
GCP
Vertex AI, Cloud Run, Cloud Functions, GKE, Pub/Sub, BigQuery, Firestore — mi opción por defecto para cargas de IA y entrega gestionada de contenedores.
Azure
AKS, Functions, Cognitive Services, Container Apps — usado donde una organización ya vive dentro del ecosistema Microsoft.
AWS
Lambda, ECS/EKS, S3, RDS, SQS/SNS detrás de CloudFront — la amplitud de primitivas para stacks de producción tradicionales.
Edge
Cloudflare Workers y Vercel Edge — lógica que corre cerca del usuario, por latencia y alcance global.
BaaS
Supabase y Firebase (Auth, Firestore, Realtime DB) — cuando un producto necesita un backend en días, no en semanas.
Entrega
Docker y Kubernetes, GitHub Actions y GitLab CI, infraestructura como código — entornos reproducibles de principio a fin.
La nube debería ser un commodity que pueda intercambiar, nunca una dependencia que sea dueña del producto.
Una cadena de herramientas Linux personal y continua
Cada fricción del flujo de trabajo se convierte en un pequeño programa.
Corro Linux a nivel administrador — tuning del kernel, endurecimiento por sysctl, unidades de systemd, runtimes de contenedores y la red por debajo. Pero la parte que define cómo trabajo es más pequeña: a lo largo de los años he escrito docenas de mis propias utilidades de línea de comandos, una a una, cada una nacida de una fricción que encontré más de una vez.
Una tarea hecha dos veces es candidata a una herramienta. El resultado es una estación de trabajo que encaja con mis manos en lugar de con los valores por defecto — y, más importante, el instinto de quien construye la herramienta en lugar de tolerar la brecha. Ese instinto es el mismo que acaba entregándose en los productos.
- Tuning del kernel, endurecimiento por sysctl, systemd, runtimes de contenedores, red
- Docenas de utilidades CLI propias, cada una resolviendo una fricción recurrente
- El reflejo de quien construye herramientas, no solo de quien las usa
Lo que aporto a una construcción.
Stack de ingeniería
- Lenguajes de sistemas principales
- Rust, Go, C/C++
- Lenguajes de alto nivel
- Python, TypeScript/JS
- Móvil — nativo
- Swift, Obj-C, Kotlin, Java
- Móvil — multiplataforma
- React Native, Flutter
- Backends
- Node, Spring, Django, Rails, Go
- Estilos de API
- REST, GraphQL
- Almacenes de datos
- PostgreSQL, BigQuery, SQLite, Redis
- Contenedores
- Docker, Kubernetes
- CI/CD
- GitHub Actions, GitLab CI
- Linux
- Nivel administrador
Nada de esto es una lista de deseos. Cada línea es una herramienta con la que he entregado — elegida, en un proyecto dado, porque era la respuesta correcta para esa capa y ese modo de fallo.
El hilo conductor a través de todo ello es el mismo temperamento: construir el sistema antes que el archivo, diseñar para las costuras, mantener la entrega reproducible y rechazar las dependencias que serían dueñas del producto más adelante.
Desacoplado por mensaje, no por esperanza.
Una topología productor / broker / consumidor
Los componentes hablan a través de un broker para que un fallo ralentice el sistema en lugar de detenerlo.
Cuando los servicios se llaman entre sí directamente, una sola dependencia lenta se convierte en una cascada. Diseño en torno a un broker de eventos en su lugar: los productores publican hechos sobre lo que ocurrió, el broker los retiene de forma durable y los consumidores procesan a su propio ritmo. El productor nunca se bloquea esperando al consumidor, y un consumidor que se queda atrás drena su backlog sin perder trabajo.
Los eventos se convierten en la fuente de verdad de lo que ocurrió — una cola de mensajes muertos atrapa lo que no se puede procesar, los reintentos son acotados e idempotentes, y un nuevo consumidor puede suscribirse al mismo stream sin que nadie cambie el productor. Esa es la propiedad que estoy comprando: la evolución independiente de partes que nunca tienen que conocerse entre sí.
- Los productores publican, los consumidores se suscriben — sin acoplamiento directo
- Un broker durable absorbe ráfagas; los consumidores procesan a su propio ritmo
- Cola de mensajes muertos y reintentos acotados e idempotentes ante fallos
De un commit a producción, con el artefacto promoviendo hacia adelante.
Un pipeline de CI/CD que trato como infraestructura innegociable. La imagen se construye una vez y se promueve sin cambios a través de cada compuerta, de modo que lo que corre en producción es byte a byte lo que pasó los tests.
De commit a canary
- 01 Commit Push a una rama. Los hooks pre-commit corren formato y lint en local para que lo obvio se atrape antes de que CI siquiera arranque.
- 02 Build y test Imagen de contenedor construida una vez, tests unitarios y de integración corridos contra ella, umbral de cobertura aplicado. El mismo artefacto promueve hacia adelante.
- 03 Escaneo Análisis estático, auditoría de dependencias y escaneo de vulnerabilidades de imagen. Un escaneo fallido bloquea el merge, no la memoria de un revisor humano.
- 04 Staging Desplegar la imagen firmada a un entorno de staging que refleja producción, correr smoke tests y un breve soak.
- 05 Promover Desplegar a producción detrás de un canary o un switch blue-green, vigilar las métricas y mantener la versión anterior a un comando de distancia.
Estado llevado sobre un socket, con los modos de fallo manejados.
Una malla de servicios sobre socket
Una gateway WebSocket al frente, conexiones con estado detrás, un almacén compartido para durabilidad.
El trabajo en tiempo real vive o muere por las partes poco glamorosas: qué pasa cuando la conexión de un cliente cae a mitad de mensaje, cuando un consumidor se queda atrás del productor, cuando el mismo evento llega dos veces. Termino conexiones WebSocket y de socket en una gateway, enruto cada una a un handler con estado y mantengo estado durable en un almacén compartido para que un cliente que reconecta pueda hacer resume en lugar de reiniciar.
La contrapresión es explícita — buffers acotados con una política definida de descartar o fusionar, nunca una cola sin límite que se come la memoria en silencio hasta que el proceso muere. La presencia se rastrea con heartbeats y claves con TTL, y la entrega es idempotente para que un transporte at-least-once no se vuelva un comportamiento at-least-twice para el usuario.
- Gateway WebSocket / socket termina y enruta conexiones
- Handlers con estado por conexión; almacén compartido para durabilidad
- Heartbeats, presencia con TTL, resume al reconectar, entrega idempotente
Primitivas de tiempo real
- Transporte
- WebSocket, sockets TCP crudos, streams gRPC
- Fan-out
- Pub/Sub, canales Redis, SSE para un solo sentido
- Contrapresión
- Buffers acotados, políticas de descarte / fusión
- Entrega
- Claves de idempotencia, at-least-once + deduplicación
- Presencia
- Heartbeats, claves con TTL, reconexión con resume
- Estado
- Actor por conexión, almacén compartido para durabilidad
Dos fragmentos más del stack en uso.
El ownership de Rust y los channels de Go resuelven el mismo problema — estado compartido bajo concurrencia — desde extremos opuestos: uno mueve el ownership para que no haya nada que compartir, el otro comparte comunicando.
// Ownership moves; the compiler proves no use-after-move.
fn consume(buf: Vec<u8>) -> usize {
buf.len() // buf is owned here, freed at the end
}
fn main() {
let data = vec![1u8, 2, 3];
let n = consume(data);
// using 'data' here would not compile: it was moved.
println!('consumed {} bytes', n);
// Borrow instead of move when the caller keeps the value.
let kept = vec![4u8, 5];
let total: u8 = kept.iter().sum();
println!('{} still owns {} items', total, kept.len());
} // Share by communicating: a generator feeds a stage over a channel.
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
} Lógica que corre cerca del usuario.
Un handler de edge en TypeScript — del tipo que despliego en Cloudflare Workers o Vercel Edge. Corre en el punto de presencia más cercano a la solicitud, de modo que una comprobación de caché o una compuerta de auth nunca paga un viaje de ida y vuelta a una región central.
// Runs at the edge: serve from cache, fall through to origin once.
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext) {
const cache = caches.default;
const hit = await cache.match(req);
if (hit) return hit;
const res = await fetch(req);
if (res.ok && req.method === 'GET') {
const cached = new Response(res.body, res);
cached.headers.set('cache-control', 'public, max-age=60');
ctx.waitUntil(cache.put(req, cached.clone()));
return cached;
}
return res;
},
}; El almacén correcto para el patrón de acceso.
No elijo un almacén de datos por costumbre. Lo elijo por la forma de las lecturas y escrituras que tiene que absorber, y acepto que la mayoría de los sistemas reales usan más de uno.
Un almacén relacional se gana la posición por defecto porque la mayoría de los datos son relacionales y las transacciones valen su coste. Pero la ruta caliente que sirve una caché, la pregunta analítica que escanea miles de millones de filas y la persistencia local-first en un teléfono son tres problemas distintos — y pretender que un solo motor responde a los tres es como los sistemas se vuelven lentos de formas difíciles de deshacer después.
La disciplina bajo todos ellos es la misma: el esquema vive como migraciones versionadas y reversibles en el repo, los índices se diseñan contra el plan de consulta real en lugar de adivinarse, y las consultas de la ruta caliente se leen y se afinan a mano aun cuando un ORM escribió el primer borrador.
PostgreSQL
El almacén relacional por defecto — transacciones, JSONB donde se gana su lugar, e índices diseñados contra el plan de consulta en lugar de adivinados.
BigQuery
Escala analítica. Almacenamiento columnar y particionado para las preguntas que escanean miles de millones de filas pero corren rara vez.
SQLite
Persistencia en dispositivo y embebida para móvil y edge — un solo archivo, cero servidor, la herramienta correcta para apps local-first.
Redis
La ruta caliente — caché, rate limiting, colas, presencia efímera y locks donde los microsegundos y los TTLs importan.
ORMs
Sequelize, Mongoose, GORM donde aceleran la entrega — pero el SQL generado se mantiene legible, y las consultas calientes se escriben a mano.
Migraciones
Esquema como migraciones versionadas y reversibles dentro del repo, de modo que una base de datos sea reproducible en lugar de arqueológica.
El kernel sobre el que corre el contenedor, tratado como parte del producto.
Un stack de defensa en profundidad
Seguridad como capas, desde el arranque firmado hasta la auditoría fuera del host, no una sola regla de firewall.
Administro Linux hasta el kernel, y trato el host como parte de la superficie de ataque en lugar de un lugar inerte donde correr un contenedor. El stack se lee de abajo hacia arriba: un arranque firmado y mínimo y una base de sysctl, usuarios de mínimo privilegio con claves SSH y sin contraseñas, un firewall default-deny, aislamiento de procesos mediante sandboxing de systemd y cgroups, y logs de auditoría enviados fuera del host para que un compromiso no pueda borrar su propio rastro en silencio.
Nada de esto es exótico — es la disciplina ordinaria de correr un host como si fuera a ser atacado, porque tarde o temprano uno lo es. El mismo instinto que construye una pequeña CLI para una fricción recurrente construye una imagen base endurecida una vez y la reutiliza en todas partes, de modo que la ruta segura es también la ruta fácil.
- Arranque firmado, módulos de kernel mínimos, base de sysctl
- nftables default-deny, sandboxing de systemd, seccomp, cgroups
- auditd y envío de logs fuera del host; actualizaciones de seguridad desatendidas
Cómo se acumuló el stack, capa por capa.
Ningún año por sí solo enseñó todo esto. Se acumuló como debe acumularse un stack — desde el vidrio que toca el usuario, hacia abajo, hasta que el kernel fue tan familiar como la vista.
- Años 1–3 Móvil nativo, de principio a fin Swift y Objective-C en iOS, Kotlin y Java en Android. Aprendí la disciplina de entregar a dispositivos en el campo — fragmentación, offline, presupuestos de batería y memoria.
- Años 3–6 Los backends detrás de las apps Node, Spring, Django y Rails sirviendo REST y GraphQL, respaldados por PostgreSQL y Redis. La app dejó de terminar en la API y empezó en el kernel.
- Años 6–9 Multiplataforma y nube React Native y Flutter donde una base de código compartida rendía; contenedores, Kubernetes y entrega multinube para que la infraestructura fuera reproducible.
- Años 9–hoy Profundidad en sistemas y distribuido Rust y Go para núcleos que deben ser correctos y rápidos, topologías dirigidas por eventos y en tiempo real, y una cadena de herramientas Linux personal y continua bajo todo ello.
Open to the right work
Si necesitas un solo ingeniero capaz de sostener todo el stack — del kernel a la nube — ese es el trabajo que hago.
If you are holding a problem that doesn't fit inside one field, that is the conversation I want.