Comprender
¿Qué intenta lograr el sistema? ¿Qué sucede actualmente? ¿Qué restricciones ya existen?
Una colección de temas de ingeniería moldeados por mi experiencia con sistemas backend, procesamiento distribuido, rendimiento, datos y evolución de software.
Un requerimiento describe qué necesita cambiar, pero no siempre explica por qué existe el problema. Intento comprender el comportamiento y las restricciones subyacentes antes de elegir una implementación.
¿Qué intenta lograr el sistema? ¿Qué sucede actualmente? ¿Qué restricciones ya existen?
Rastreo el comportamiento a través del código, los datos y las interacciones relevantes para identificar dónde está el problema real.
Cuando hay varias soluciones posibles, considero sus ventajas y desventajas en lugar de asumir que el primer enfoque es el correcto.
Verifico que el cambio resuelva el problema original y se comporte correctamente dentro del resto del sistema.
No veo la arquitectura como una plantilla fija. La estructura adecuada depende de las responsabilidades del sistema, sus restricciones, el crecimiento esperado y el tipo de cambios que deberá soportar.
Antes de definir servicios o capas, intento entender qué responsabilidades pertenecen juntas y dónde deben existir límites claros entre las distintas responsabilidades.
No todos los problemas necesitan servicios distribuidos o infraestructura adicional. Prefiero introducir complejidad cuando resuelve un requerimiento concreto del sistema.
Los sistemas evolucionan. Intento aislar responsabilidades y cambios para que extender una parte del sistema no afecte innecesariamente todo lo que la rodea.
Prefiero medir antes de optimizar. El tiempo de procesamiento, la carga de la base de datos, el uso de memoria, las asignaciones y las operaciones costosas pueden revelar cuellos de botella muy distintos, incluso cuando el sistema parece lento desde fuera.
Intento identificar el costo real de una operación antes de decidir que necesita optimización. Sin medición, la optimización puede convertirse fácilmente en una suposición.
A veces, la mejor optimización es evitar una operación por completo: una consulta innecesaria a la base de datos, un cálculo, transformación, asignación o búsqueda de estado repetidos.
Las abstracciones pueden mejorar la mantenibilidad, pero también tienen implicaciones de ejecución y memoria. Considero estas compensaciones al trabajar en rutas sensibles al rendimiento.
Un cambio que se ve mejor de forma aislada puede comportarse distinto bajo una carga de trabajo real. Utilizo escenarios de alto volumen y estrés para comparar el comportamiento antes y después de una optimización.
Cuando aumenta la carga de trabajo, simplemente añadir más recursos no siempre es suficiente. Pienso en dónde se acumula el trabajo, cómo interactúan los componentes y qué partes del sistema necesitan escalar de forma independiente.
El aumento del tráfico puede revelar cuellos de botella en lugares inesperados. Reviso las etapas de procesamiento, las colas, las operaciones de base de datos y los recursos compartidos para entender dónde aumenta la presión.
Más trabajo en paralelo no significa automáticamente mayor capacidad de procesamiento. La concurrencia debe controlarse de acuerdo con la capacidad del sistema y los recursos de los que depende.
Cuando distintas cargas de trabajo tienen características diferentes, aislarlas puede evitar que un tipo de trabajo afecte la capacidad de procesamiento de otro.
Las decisiones de escalabilidad deben considerar CPU, memoria, almacenamiento, red, capacidad de base de datos, latencia y complejidad operativa, en lugar de centrarse en una sola métrica.
La forma en que los datos se almacenan, consultan y sincronizan afecta el comportamiento de todo el backend. Considero el propósito de los datos, sus patrones de acceso, requisitos de consistencia y la carga de trabajo que el sistema debe soportar.
Los modelos de datos deben representar el dominio y las relaciones que importan para el sistema. Parto de cómo se utilizan los datos en lugar de diseñar tablas de forma aislada.
Los mismos datos pueden requerir estructuras diferentes según la frecuencia con la que se leen, escriben, filtran, agregan o comparten entre distintas partes del sistema.
El almacenamiento relacional y no relacional resuelven problemas distintos. Considero las características de la carga de trabajo antes de decidir dónde y cómo debe persistirse la información.
El estado en caché o temporal puede mejorar el rendimiento, pero también introduce consideraciones de consistencia y ciclo de vida. Intento hacer explícitas esas responsabilidades.
El software rara vez se mantiene en el estado para el que fue diseñado originalmente. Los requerimientos cambian, las cargas de trabajo crecen, las suposiciones se vuelven obsoletas y aparecen nuevas restricciones. Veo la evolución como parte del proceso de ingeniería, no como algo que ocurre después del desarrollo.
Antes de cambiar un sistema existente, intento entender su comportamiento actual, dependencias, datos y las razones detrás de sus restricciones importantes.
Los cambios grandes pueden aumentar el riesgo. Cuando es posible, prefiero separar los cambios en pasos más pequeños que puedan entenderse, probarse y validarse de manera independiente.
No todo en un sistema existente necesita reemplazarse. Intento distinguir entre lo que debe cambiar y lo que puede seguir cumpliendo su propósito.
Idealmente, un cambio exitoso debe dejar el sistema en un estado en el que el siguiente cambio sea más fácil de comprender e implementar, en lugar de añadir otra capa de complejidad accidental.
Intento evaluar las soluciones según el problema que deben resolver, las restricciones del sistema y las consecuencias que introducen. Rara vez existe una elección técnica universalmente correcta.
¿Qué intentamos resolver realmente?
¿Qué limitaciones ya existen?
¿Qué ganamos y a qué renunciamos?
¿Cómo afectará esta decisión al sistema más adelante?