Una arquitectura no se juzga solo por la funcionalidad que soporta, sino por cómo la soporta: cuán disponible, rápida, segura y modificable es. Estas propiedades se denominan atributos de calidad y rara vez surgen por casualidad. Se consiguen mediante tácticas arquitectónicas: decisiones de diseño concretas y catalogadas que influyen sobre la respuesta del sistema ante un estímulo. El concepto proviene del Software Engineering Institute (SEI) y del libro Software Architecture in Practice. En esta lección estudiaremos las tácticas más importantes para disponibilidad, rendimiento, seguridad y modificabilidad, con ejemplos concretos y una tabla de referencia que relaciona cada atributo con sus tácticas.

Contenido

  1. Qué es un atributo de calidad y qué es una táctica
  2. Escenarios de atributo de calidad
  3. Tácticas de disponibilidad
  4. Tácticas de rendimiento
  5. Tácticas de seguridad
  6. Tácticas de modificabilidad
  7. Tabla resumen: atributo → tácticas
  8. Errores comunes y consejos
  9. Ejercicios
  10. Conclusión

  1. Qué es un atributo de calidad y qué es una táctica

Un atributo de calidad (quality attribute o "-ilidad") es una propiedad medible que indica cómo de bien satisface el sistema las necesidades de sus interesados: disponibilidad, rendimiento, seguridad, modificabilidad, escalabilidad, usabilidad, etc.

Una táctica es una decisión de diseño que afecta a la respuesta ante un estímulo concreto relacionado con un atributo. A diferencia de un patrón (que combina varias tácticas y estructuras), la táctica es un átomo de diseño: granular y combinable.

  • Patrón = receta completa (p. ej. Circuit Breaker, Cache-Aside).
  • Táctica = ingrediente (p. ej. "detectar fallos con heartbeat", "introducir concurrencia").

  1. Escenarios de atributo de calidad

Para diseñar y medir, los atributos de calidad se expresan como escenarios con seis partes. Esto convierte un requisito vago ("el sistema debe ser rápido") en algo verificable.

Parte Descripción Ejemplo
Fuente Quién genera el estímulo Usuario externo
Estímulo El evento que llega 1000 peticiones/seg
Artefacto Qué parte se ve afectada API de catálogo
Entorno Condiciones del sistema Carga normal
Respuesta Qué hace el sistema Procesa las peticiones
Medida Criterio de éxito Latencia p95 < 200 ms

Escenario completo de ejemplo: "Bajo carga normal (1000 req/s), la API de catálogo responde con una latencia p95 inferior a 200 ms". Las tácticas se eligen para satisfacer escenarios como este.

  1. Tácticas de disponibilidad

La disponibilidad es la capacidad del sistema de estar operativo y accesible cuando se le necesita. Las tácticas se agrupan en tres categorías: detectar fallos, recuperarse de ellos y prevenirlos.

Detección de fallos:

  • Ping/Echo y Heartbeat: un componente envía señales periódicas; su ausencia indica caída.
  • Monitorización y timeouts: se vigila el tiempo de respuesta y se asume fallo al expirar.
  • Comprobación de salud (health check): endpoints que verifican el estado interno.

Recuperación ante fallos:

  • Redundancia activa/pasiva: réplicas listas para asumir la carga.
  • Reintentos con backoff: reintentar operaciones idempotentes con espera creciente.
  • Circuit Breaker: dejar de llamar a un servicio que falla para no agravar la situación.
  • Degradación elegante: ofrecer servicio reducido en lugar de caer por completo.

Prevención de fallos:

  • Eliminación del servicio del pool ante anomalías.
  • Transacciones para mantener consistencia.

Ejemplo de Circuit Breaker (declarativo, con Resilience4j):

@CircuitBreaker(name = "inventario", fallbackMethod = "stockPorDefecto")
public int consultarStock(String producto) {
    return clienteInventario.obtenerStock(producto); // llamada remota
}

// Método de respaldo: se invoca cuando el circuito está abierto o la llamada falla
public int stockPorDefecto(String producto, Throwable t) {
    return 0; // degradación elegante: asumimos sin stock en vez de fallar
}

Explicación: el @CircuitBreaker envuelve la llamada remota. Si el servicio de inventario acumula fallos, el circuito se "abre" y deja de invocarlo, devolviendo el fallbackMethod. Esto evita saturar un servicio caído y protege la disponibilidad global. La medida típica del escenario sería "el sistema sigue respondiendo aunque inventario esté caído".

  1. Tácticas de rendimiento

El rendimiento mide la capacidad de responder dentro de unos límites de tiempo (latencia) y volumen (throughput). Las tácticas se dividen en gestionar la demanda de recursos y gestionar los propios recursos.

Gestión de la demanda:

  • Caché: almacenar resultados costosos para reutilizarlos.
  • Limitación de tasa (rate limiting) y muestreo de eventos.
  • Reducir overhead: menos capas, menos serialización innecesaria.

Gestión de recursos:

  • Concurrencia / paralelismo: procesar en paralelo.
  • Aumentar recursos / escalado: más CPU, memoria o nodos.
  • Pool de conexiones / hilos: reutilizar recursos caros de crear.
  • Procesamiento asíncrono: desacoplar la petición de su procesamiento.

Ejemplo de táctica de caché (patrón Cache-Aside):

public Producto obtenerProducto(String id) {
    Producto cacheado = cache.get(id);          // 1. miramos primero la caché
    if (cacheado != null) {
        return cacheado;                         // 2. acierto: respuesta inmediata
    }
    Producto p = repositorio.buscar(id);         // 3. fallo: vamos a la BD (lento)
    cache.put(id, p, Duration.ofMinutes(10));    // 4. guardamos con expiración (TTL)
    return p;
}

Explicación paso a paso: (1) consultamos la caché; (2) si hay acierto evitamos la base de datos; (3) si no, recurrimos al origen lento; (4) almacenamos el resultado con un tiempo de vida (TTL) para no servir datos eternamente obsoletos. La caché reduce drásticamente la latencia y la carga del backend, a cambio de gestionar la coherencia de los datos.

  1. Tácticas de seguridad

La seguridad protege la confidencialidad, integridad y disponibilidad de los datos y servicios. Las tácticas se agrupan en resistir, detectar y recuperarse de ataques.

Resistir ataques:

  • Autenticación: verificar la identidad (tokens, OAuth2, certificados).
  • Autorización: controlar qué puede hacer cada identidad (RBAC, ABAC).
  • Cifrado: datos en tránsito (TLS) y en reposo.
  • Validación de entrada: prevenir inyección y datos maliciosos.
  • Mínimo privilegio: cada componente solo con los permisos que necesita.

Detectar ataques:

  • Auditoría y registro (logging) de eventos de seguridad.
  • Detección de intrusiones y anomalías.

Recuperarse de ataques:

  • Revocación de credenciales, copias de seguridad, trazabilidad para forense.

Ejemplo de autorización declarativa (Spring Security):

@PreAuthorize("hasRole('ADMIN')")          // solo administradores
public void eliminarUsuario(String id) {
    repositorio.eliminar(id);
}

@PreAuthorize("#propietario == authentication.name") // el dueño del recurso
public Pedido verPedido(String propietario, String pedidoId) {
    return repositorio.buscar(pedidoId);
}

Explicación: @PreAuthorize evalúa una expresión antes de ejecutar el método. El primer caso aplica control de acceso basado en roles (RBAC); el segundo, una regla más fina que comprueba que quien llama sea el propietario del recurso. Combinado con validación de entrada y cifrado en tránsito, conforma una defensa en profundidad. Recuerda que estas decisiones tienen implicaciones de cumplimiento (por ejemplo, protección de datos) y deben revisarse con criterio profesional.

  1. Tácticas de modificabilidad

La modificabilidad es la facilidad y el coste con que se realizan cambios. Es el atributo más ligado a los principios de diseño vistos anteriormente. Las tácticas buscan localizar el impacto del cambio, prevenir efectos colaterales y diferir el binding.

Reducir el tamaño de los módulos:

  • Dividir módulos grandes en piezas cohesionadas.

Aumentar la cohesión:

  • Agrupar responsabilidades afines (mantener la cohesión funcional).

Reducir el acoplamiento:

  • Encapsular detrás de interfaces.
  • Usar intermediarios (fachadas, adaptadores, brokers).
  • Restringir dependencias (reglas de capas, módulos).
  • Refactorizar para eliminar duplicación.

Diferir el momento de enlace (binding):

  • Configuración externa, inyección de dependencias, plugins, feature flags.

Ejemplo de diferir el binding mediante configuración externa e inyección:

public interface PasarelaPago {
    void cobrar(double importe);
}

@Service
public class ServicioCheckout {
    private final PasarelaPago pasarela;
    // la implementación concreta se decide fuera del código, por configuración
    public ServicioCheckout(PasarelaPago pasarela) { this.pasarela = pasarela; }
    public void finalizar(double total) { pasarela.cobrar(total); }
}
# application.yml: cambiar de proveedor de pago sin tocar el código compilado
pagos:
  proveedor: stripe   # basta con cambiar este valor a "paypal"

Explicación: el servicio depende de la interfaz PasarelaPago, no de un proveedor concreto. Qué implementación se inyecta se decide por configuración (application.yml). Cambiar de Stripe a PayPal no requiere modificar ni recompilar la lógica de negocio: el impacto del cambio queda localizado, que es justo el objetivo de la modificabilidad.

  1. Tabla resumen: atributo → tácticas

Atributo de calidad Tácticas principales Ejemplo de mecanismo
Disponibilidad Detección (heartbeat, health check), recuperación (redundancia, reintentos, circuit breaker, degradación), prevención Resilience4j, réplicas, balanceadores
Rendimiento Caché, concurrencia, asíncrono, pools, escalado, rate limiting Redis, hilos, colas, autoscaling
Seguridad Autenticación, autorización, cifrado, validación, mínimo privilegio, auditoría OAuth2/TLS, RBAC, WAF, logs
Modificabilidad Reducir acoplamiento, aumentar cohesión, encapsular, diferir binding Interfaces, DI, plugins, feature flags
Escalabilidad Particionado, statelessness, balanceo de carga Sharding, servicios sin estado
Testabilidad Inyección de dependencias, interfaces, control de estado Mocks, puertos y adaptadores

Nota importante: las tácticas interactúan y compiten. Añadir redundancia (disponibilidad) puede complicar la consistencia; añadir cifrado (seguridad) penaliza el rendimiento; añadir capas de abstracción (modificabilidad) añade latencia. La arquitectura consiste en encontrar el equilibrio adecuado según los escenarios prioritarios.

Errores Comunes y Consejos

  • Optimizar atributos que nadie ha priorizado. Sin escenarios concretos, se invierte esfuerzo donde no aporta. Define escenarios medibles antes de elegir tácticas.
  • Ignorar las contrapartidas (trade-offs). Cada táctica tiene un coste en otro atributo. Hazlo explícito.
  • Confundir táctica con patrón. El circuit breaker es un patrón que materializa varias tácticas; no las mezcles conceptualmente.
  • Añadir caché sin estrategia de invalidación. Es la fuente número uno de bugs de datos obsoletos. Define siempre TTL o invalidación.
  • Tratar la seguridad como una capa final. Debe diseñarse desde el principio (security by design) y revisarse profesionalmente, especialmente en dominios regulados.
  • Consejo: documenta las decisiones de tácticas y sus trade-offs en registros de decisiones de arquitectura (ADR).

Ejercicios

Ejercicio 1. Escribe un escenario de atributo de calidad de 6 partes para la disponibilidad de un servicio de pagos.

Ejercicio 2. Un endpoint consulta una tabla de configuración que cambia una vez al día pero se lee miles de veces por minuto, y va lento. ¿Qué táctica de rendimiento aplicarías y por qué?

Ejercicio 3. Indica qué táctica de modificabilidad permite cambiar el proveedor de envío de SMS sin recompilar la aplicación, y esboza el diseño.

Soluciones

Solución 1. Por ejemplo:

Fuente: nodo del servicio de pagos. Estímulo: el nodo deja de responder. Artefacto: servicio de pagos. Entorno: operación normal. Respuesta: un nodo redundante asume el tráfico automáticamente. Medida: la disponibilidad mensual se mantiene por encima del 99,95% y la conmutación tarda menos de 5 segundos.

Solución 2. Táctica de caché: los datos cambian una vez al día (baja volatilidad) pero se leen masivamente. Cachear el resultado con un TTL de minutos (o invalidación al actualizar) elimina la inmensa mayoría de accesos al origen lento, reduciendo latencia y carga. Encaja perfectamente con el patrón Cache-Aside.

Solución 3. Táctica de diferir el binding mediante interfaz + inyección + configuración externa:

public interface PasarelaSms { void enviar(String numero, String texto); }

@Service
public class ServicioAvisos {
    private final PasarelaSms sms;
    public ServicioAvisos(PasarelaSms sms) { this.sms = sms; }
    public void avisar(String numero) { sms.enviar(numero, "Aviso"); }
}
// La implementación concreta (Twilio, Vonage...) se selecciona por configuración,
// sin tocar ServicioAvisos ni recompilar la lógica de negocio.

Conclusión

Las tácticas arquitectónicas son el repertorio de decisiones concretas con el que un arquitecto persigue los atributos de calidad. Hemos visto cómo expresar requisitos como escenarios medibles y hemos recorrido tácticas para disponibilidad (detección, recuperación, prevención), rendimiento (caché, concurrencia, asincronía), seguridad (resistir, detectar, recuperarse) y modificabilidad (reducir acoplamiento, diferir binding). La tabla resumen y la advertencia sobre los trade-offs son las herramientas que llevarás contigo. Toda decisión técnica tiene un coste y una vida útil; cuando esas decisiones se acumulan sin gestionarse, generan deuda técnica, el tema central de la siguiente y última lección de este módulo.

Curso de Arquitectura de Aplicaciones

Módulo 1: Fundamentos de la Arquitectura de Aplicaciones

Módulo 2: Principios y Tácticas de Diseño

Módulo 3: Estilos y Patrones Arquitectónicos

Módulo 4: Arquitecturas Distribuidas y Microservicios

Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería

Módulo 6: Diseño Dirigido por el Dominio (DDD)

Módulo 7: Datos y Persistencia

Módulo 8: Arquitectura en la Nube y Despliegue

Módulo 9: Calidad, Seguridad y Observabilidad

Módulo 10: Evolución, Gobernanza y Casos Prácticos

© Copyright 2026. Todos los derechos reservados