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 credencial

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.

0+

años entregando aplicaciones móviles a usuarios reales

0

plataformas móviles en producción: iOS, Android, Windows

0

nubes operadas sin atadura a un proveedor: GCP, Azure, AWS

0

lenguajes principales dominados en profundidad: Rust, Go, Python, TypeScript

El stack de lenguajes

Un stack elegido por capa, no por moda.

superficie — usuarios, modelos, datos TypeScript / JS Node, navegador, edge. Python IA, datos, computación científica, automatización. Go Servicios de red concurrentes. Rust Núcleos concurrentes y seguros en memoria. C / C++ Cercano al hardware. hardware — silicio, registros, firmware

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
Una pequeña pieza de lo real

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
}
Cuatro áreas, en profundidad

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
Topología multinube

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.

Núcleo de la app Docker · K8s · IaC GCP Cloud Run · GKE · Vertex AI Azure AKS · Functions · Apps AWS Lambda · EKS · S3 · RDS Edge CF Workers · Vercel Edge
01

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.

02

Azure

AKS, Functions, Cognitive Services, Container Apps — usado donde una organización ya vive dentro del ecosistema Microsoft.

03

AWS

Lambda, ECS/EKS, S3, RDS, SQS/SNS detrás de CloudFront — la amplitud de primitivas para stacks de producción tradicionales.

04

Edge

Cloudflare Workers y Vercel Edge — lógica que corre cerca del usuario, por latencia y alcance global.

05

BaaS

Supabase y Firebase (Auth, Firestore, Realtime DB) — cuando un producto necesita un backend en días, no en semanas.

06

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.

El instinto de quien construye herramientas
fricción hecha dos veces herramienta ~/.local/bin la cadena de herramientas se acumula

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
El stack, de un vistazo

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.

Arquitectura dirigida por eventos

Desacoplado por mensaje, no por esperanza.

productor app · dispositivo productor servicio broker Pub/Sub · cola consumidor consumidor consumidor mensaje muerto

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
Pipeline de entrega

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

  1. 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.
  2. 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.
  3. 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.
  4. 04 Staging Desplegar la imagen firmada a un entorno de staging que refleja producción, correr smoke tests y un breve soak.
  5. 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.
rollback — versión anterior a un comando de distancia commit build+ test escaneo staging canarydespliegue prod
Servicios en tiempo real

Estado llevado sobre un socket, con los modos de fallo manejados.

cliente · ws cliente · ws cliente · ws gatewayterminar · enrutar handler handler almacén de estado compartido

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
Una segunda pieza de lo real

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
}
Handler en el edge

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;
  },
};
La capa de almacenamiento de datos

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.

01

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.

02

BigQuery

Escala analítica. Almacenamiento columnar y particionado para las preguntas que escanean miles de millones de filas pero corren rara vez.

03

SQLite

Persistencia en dispositivo y embebida para móvil y edge — un solo archivo, cero servidor, la herramienta correcta para apps local-first.

04

Redis

La ruta caliente — caché, rate limiting, colas, presencia efímera y locks donde los microsegundos y los TTLs importan.

05

ORMs

Sequelize, Mongoose, GORM donde aceleran la entrega — pero el SQL generado se mantiene legible, y las consultas calientes se escriben a mano.

06

Migraciones

Esquema como migraciones versionadas y reversibles dentro del repo, de modo que una base de datos sea reproducible en lugar de arqueológica.

Stack de endurecimiento de Linux

El kernel sobre el que corre el contenedor, tratado como parte del producto.

Actualizaciones Parches de seguridad desatendidos, imágenes reproducibles Auditoría auditd, retención de journald, envío de logs fuera del host Proceso sandboxing de systemd, seccomp, namespaces, cgroups Red nftables default-deny, fail2ban, puertos segmentados Identidad Usuarios de mínimo privilegio, política sudo, solo claves SSH Arranque y kernel Arranque firmado, módulos mínimos, base de sysctl externo — superficie expuesta, vigilada y registrada

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
El recorrido

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.

NextDefense Telecom