Arquitectura de Software

Domain-Driven Design: Supporting Subdomains, cómo diseñar las capacidades que sostienen el negocio sin sobreingeniería.

Diseña una gestión de devoluciones proporcional a sus reglas y riesgos. Aprende a elegir funciones, modelos pequeños, automatizaciones o herramientas existentes, con TypeScript, pruebas propuestas y criterios para evolucionar sin sobreingeniería.

Cargando tus opciones de aprendizaje…

La devolución no diferencia a la tienda, pero puede detenerla

La tienda del artículo anterior se diferencia por sus recomendaciones personalizadas. Sin embargo, sus operadores todavía revisan devoluciones buscando pedidos, contando unidades y consultando instrucciones que cambian entre documentos.

El equipo propone automatizar ese trabajo. Una opción es construir una plataforma de devoluciones con un motor de reglas configurable, múltiples servicios y un bus de eventos. Otra es añadir condiciones directamente a la pantalla administrativa.

Entre ambas existe una alternativa: modelar las reglas necesarias con claridad y una implementación proporcional.

Un Supporting Subdomain, o subdominio de soporte, puede contener conocimiento particular del negocio y necesitar desarrollo propio sin convertirse en el foco principal de diferenciación. Diseñarlo bien significa que sostenga la operación sin absorber complejidad que los requisitos no justifican.

Qué caracteriza a un subdominio de soporte

Los Supporting Subdomains mantienen capacidades necesarias para operar, aunque no constituyan la ventaja diferenciadora buscada por la organización. Pueden requerir reglas propias, adaptaciones y mantenimiento especializado. La documentación de Microsoft utiliza esta distinción frente a capacidades centrales y problemas genéricos ya resueltos en la industria. Análisis del dominio.

En nuestra tienda hipotética:

Comercio electrónico especializado
  |
  +-- Core Domain: selección y recomendaciones personalizadas
  |
  +-- Supporting: gestión operativa de devoluciones
  |
  +-- Generic: autenticación convencional

Esta clasificación responde a su estrategia actual. Una empresa cuya propuesta principal fuera gestionar devoluciones para otras tiendas podría clasificar esa capacidad de otra manera.

Tampoco significa que «soporte» sea sinónimo de atención al cliente. La palabra describe su papel estratégico, no un departamento.

No es una carpeta ni una capa

Supporting Subdomain clasifica una capacidad del negocio. Un Bounded Context delimita un modelo para resolver problemas; no existe una correspondencia automática entre ambos.

Crear src/supporting no determina la calidad o el alcance de la solución. Una capacidad de soporte puede resolverse en un módulo acotado, necesitar un modelo más amplio o apoyarse en una herramienta existente.

Proporcionalidad no significa baja calidad

La devolución puede afectar dinero, existencias y confianza del cliente. Un fallo puede costar más que una mejora temporal del recomendador.

Por eso conviene separar tres dimensiones:

  • Papel estratégico: cuánto contribuye a la diferenciación.
  • Criticidad: qué consecuencias tiene un error o una interrupción.
  • Complejidad real: qué reglas, estados y coordinaciones debe manejar.

Un proceso de soporte crítico requiere controles, pruebas y operación adecuados. La etiqueta no autoriza a omitir seguridad, trazabilidad o revisión de reglas.

La proporcionalidad consiste en no añadir mecanismos que no resuelven una necesidad. No consiste en reducir garantías necesarias para cumplir correctamente la responsabilidad.

Evans propone concentrar el diseño especializado en el núcleo y justificar el esfuerzo del resto por cómo lo sostiene. Eso orienta prioridades; no convierte los componentes de soporte en trabajo desechable. DDD Reference: Core Domain.

Identificar el problema operativo antes de elegir patrones

Los operadores de la tienda describen tres dificultades:

  1. No saben cuántas unidades de una línea ya tienen una devolución admitida.
  2. Las solicitudes por daños necesitan revisión y se mezclan con devoluciones comerciales ordinarias.
  3. No queda registrado qué regla se aplicó al admitir una solicitud.

La primera entrega no necesita predecir fraude ni diseñar una plataforma para otras empresas. Necesita evaluar una política concreta y registrar decisiones.

También deben aclararse casos que cambian el alcance: líneas parcialmente devueltas, pedidos con entregas separadas, reemplazos y solicitudes simultáneas.

No todos tienen que resolverse mediante automatización completa desde el inicio. Pero no deben desaparecer por falta de una pantalla o un campo.

Una política didáctica con límites explícitos

Para el ejemplo utilizaremos una política comercial ficticia:

  • La recepción ordinaria se evalúa sobre unidades entregadas y sin una devolución admitida previamente.
  • El plazo comercial es de 30 periodos completos de 24 horas desde la entrega, incluido el instante final.
  • Una solicitud por daños se deriva a revisión.
  • Un producto abierto también se deriva a revisión en este flujo simplificado.
  • Cumplir las condiciones permite admitir la solicitud para recepción, no autoriza un reembolso.

No representa un plazo legal ni cubre garantías u otras obligaciones, que deben evaluarse por separado con los responsables correspondientes. El código ilustra una decisión operativa acotada, no una política real lista para adoptar.

Al definir el plazo en horas transcurridas evitamos mezclarlo con días del calendario local. Si el negocio trabaja con fechas civiles, hará falta otra regla.

Los pedidos con entregas parciales requieren hechos por unidad o entrega. La versión simplificada supone que la línea evaluada tiene un único instante de entrega.

Una función pequeña que exprese la decisión

En returns/evaluate-intake.ts:

export interface ReturnFacts {
  readonly purchasedUnits: number;
  readonly admittedUnits: number;
  readonly requestedUnits: number;
  readonly deliveredAtMs: number;
  readonly requestedAtMs: number;
  readonly reason: "CHANGE_OF_MIND" | "DAMAGED";
  readonly packaging: "SEALED" | "OPENED";
}

export type IntakeDecision =
  | { readonly kind: "ELIGIBLE_FOR_INTAKE" }
  | {
      readonly kind: "MANUAL_REVIEW";
      readonly reason: "DAMAGE_REPORTED" | "OPENED_PRODUCT";
    }
  | {
      readonly kind: "OUTSIDE_COMMERCIAL_WINDOW";
    };

export function evaluateIntake(facts: ReturnFacts): IntakeDecision {
  const validUnits =
    Number.isSafeInteger(facts.purchasedUnits) &&
    facts.purchasedUnits >= 1 &&
    Number.isSafeInteger(facts.admittedUnits) &&
    facts.admittedUnits >= 0 &&
    facts.admittedUnits <= facts.purchasedUnits &&
    Number.isSafeInteger(facts.requestedUnits) &&
    facts.requestedUnits >= 1;

  if (
    !validUnits ||
    facts.requestedUnits >
      facts.purchasedUnits - facts.admittedUnits
  ) {
    throw new Error("Cantidad solicitada inválida");
  }

  if (
    !Number.isSafeInteger(facts.deliveredAtMs) ||
    !Number.isSafeInteger(facts.requestedAtMs) ||
    facts.deliveredAtMs < 0 ||
    facts.requestedAtMs < facts.deliveredAtMs
  ) {
    throw new Error("Instantes de devolución inválidos");
  }

  if (facts.reason === "DAMAGED") {
    return { kind: "MANUAL_REVIEW", reason: "DAMAGE_REPORTED" };
  }

  const commercialWindowMs = 30 * 24 * 60 * 60 * 1000;
  const elapsedMs = facts.requestedAtMs - facts.deliveredAtMs;

  if (elapsedMs > commercialWindowMs) {
    return { kind: "OUTSIDE_COMMERCIAL_WINDOW" };
  }

  if (facts.packaging === "OPENED") {
    return { kind: "MANUAL_REVIEW", reason: "OPENED_PRODUCT" };
  }

  return { kind: "ELIGIBLE_FOR_INTAKE" };
}

El bloque es autocontenido como lógica didáctica y no usa dependencias externas. Presupone que los valores enumerados se validaron en ejecución antes de llamar a la función: TypeScript no valida un JSON.

Los hechos de entrega y unidades compradas deben proceder de fuentes autorizadas, no de valores enviados libremente por el solicitante. El instante de recepción de la solicitud debe fijarlo el sistema según el acuerdo operativo.

La función no depende de la base de datos, del reloj global ni del recomendador. Recibe hechos y devuelve una decisión que se puede probar.

Los nombres importan. ELIGIBLE_FOR_INTAKE no significa REFUND_APPROVED. Confundirlos produciría un error de negocio aunque el código fuera sencillo.

La precedencia de reglas también es conocimiento

Los daños se derivan a revisión antes de aplicar el plazo comercial ordinario. No afirmamos que una reclamación por daños deba rechazarse por superar ese plazo.

Una cantidad inválida, en cambio, no se acepta por indicar daños. Primero comprobamos que la solicitud sea coherente con las unidades consideradas.

La precedencia debe discutirse con quienes gestionan el proceso y probarse. No es un detalle que una IA deba elegir por intuición.

Probar lo que puede causar errores

Este ejemplo de pruebas usa APIs integradas de Node.js. Presupone compilación de TypeScript a JavaScript ESM y complementa el archivo anterior. Son pruebas propuestas; no se han ejecutado como parte del artículo.

import test from "node:test";
import assert from "node:assert/strict";
import {
  evaluateIntake,
  type ReturnFacts,
} from "./evaluate-intake.js";

const deliveredAtMs = Date.UTC(2026, 0, 1);
const windowMs = 30 * 24 * 60 * 60 * 1000;

const normalRequest: ReturnFacts = {
  purchasedUnits: 2,
  admittedUnits: 0,
  requestedUnits: 1,
  deliveredAtMs,
  requestedAtMs: deliveredAtMs + 5 * 24 * 60 * 60 * 1000,
  reason: "CHANGE_OF_MIND",
  packaging: "SEALED",
};

test("incluye el instante final del plazo comercial", () => {
  assert.deepEqual(evaluateIntake({
    ...normalRequest,
    requestedAtMs: deliveredAtMs + windowMs,
  }), { kind: "ELIGIBLE_FOR_INTAKE" });
});

test("distingue el instante inmediatamente posterior", () => {
  assert.deepEqual(evaluateIntake({
    ...normalRequest,
    requestedAtMs: deliveredAtMs + windowMs + 1,
  }), { kind: "OUTSIDE_COMMERCIAL_WINDOW" });
});

test("un daño requiere revisión aunque exceda ese plazo", () => {
  assert.deepEqual(evaluateIntake({
    ...normalRequest,
    reason: "DAMAGED",
    requestedAtMs: deliveredAtMs + windowMs + 1,
  }), { kind: "MANUAL_REVIEW", reason: "DAMAGE_REPORTED" });
});

test("no admite más unidades de las que quedan disponibles", () => {
  assert.throws(() => evaluateIntake({
    ...normalRequest,
    admittedUnits: 1,
    requestedUnits: 2,
  }));
});

test("un producto abierto no se admite automáticamente", () => {
  assert.deepEqual(evaluateIntake({
    ...normalRequest,
    packaging: "OPENED",
  }), { kind: "MANUAL_REVIEW", reason: "OPENED_PRODUCT" });
});

Las pruebas documentan límites y precedencia. No prueban autenticación, persistencia ni comportamiento ante solicitudes concurrentes.

Agregar una clase abstracta para cada condición no aportaría esas garantías. Hay que identificar dónde se necesitan y verificarlas en la implementación correspondiente.

De la función al proceso real

Una entrega operativa necesita conectar la regla con el trabajo cotidiano:

  1. Autenticar y autorizar a quien presenta o revisa la solicitud.
  2. Validar el formato y obtener hechos confiables de la línea entregada.
  3. Identificar si la misma solicitud ya existe.
  4. Evaluar y registrar la decisión con la versión de política aplicable.
  5. Admitir o derivar a revisión según el proceso.
  6. Presentar un resultado comprensible al operador o solicitante.

La decisión y la reserva de unidades admitidas deben registrarse con protección frente a concurrencia. Si dos solicitudes leen el mismo saldo y luego lo modifican sin coordinación, la función pura no evita que ambas consuman las mismas unidades.

También hay que definir cuándo una admisión deja de consumir unidades, por ejemplo al cancelarse, y cómo se conserva su historial.

Estos requisitos pueden resolverse con una aplicación y transacciones locales adecuadas. No obligan automáticamente a introducir servicios distribuidos.

Si los hechos proceden de otro contexto, se necesitan contratos explícitos y traducción del consumidor. Una consulta HTTP no comparte la transacción local. El diseño debe reconocer cómo verifica o concilia los hechos utilizados.

No implementamos aquí ese almacenamiento ni un endpoint: mostrar una función no equivale a entregar el sistema completo de devoluciones.

Comparar una solución proporcional con otra sobreingenierizada

Para la primera versión, una organización posible sería:

returns/
  evaluate-intake.ts
  intake-service.ts
  intake-http.ts
  intake-storage.ts
  evaluate-intake.test.ts

Los nombres son ilustrativos. La regla, la coordinación, la entrada y la persistencia tienen responsabilidades claras sin necesitar una jerarquía extensa.

Una propuesta más compleja podría incluir:

Motor universal de políticas
   -> plugins de condiciones
   -> compilador de reglas
   -> bus de decisiones
   -> servicios separados por condición

Esos elementos no son incorrectos por definición. Podrían tener sentido con muchas políticas, responsables no técnicos que deban editarlas o necesidades de escala y aislamiento concretas.

Para nuestra regla inicial, añaden mecanismos que el problema todavía no exige. Su coste incluye pruebas del propio motor, operación y aprendizaje del equipo.

La comparación útil no es «función buena, arquitectura mala». Es identificar qué requisito justifica cada pieza y qué riesgo resuelve.

Una matriz de decisiones técnicas

Como orientación inicial para esta capacidad:

Situación observada              Alternativa inicial
Pocas reglas estables            Función o modelo pequeño con pruebas
Pasos administrativos repetidos  Automatización acotada y auditable
Herramienta que encaja            Configuración y adaptación limitada
Muchas transiciones relevantes   Modelo explícito de estados y permisos
Reglas editables por operadores  Evaluar configuración gobernada
Coordinación real entre sistemas Contratos y manejo de fallos necesarios

No es una receta automática. La selección debe contrastarse con criticidad, volumen, frecuencia de cambios y capacidad de mantenimiento.

Si las reglas crecen y resulta difícil expresar transiciones válidas, introducir un modelo más rico puede reducir complejidad. Hacerlo no requiere convertir todos los elementos del catálogo táctico de DDD en obligatorios.

El criterio sigue siendo la claridad y corrección del proceso, no mantener para siempre la menor cantidad de archivos.

Comprar, adaptar o desarrollar

Una herramienta existente puede cubrir seguimiento de solicitudes, formularios y trabajo de operadores. Antes de construir, comprueba si permite expresar las reglas particulares, exportar registros y mantener los controles necesarios.

No des por hecho que una solución disponible encaja porque utiliza la palabra «devoluciones». Evalúa con ejemplos de tu proceso.

Un desarrollo acotado puede ser razonable si las diferencias son pequeñas y se comprende su mantenimiento. No necesita convertirse en una plataforma configurable para cualquier tienda.

También puede combinarse una herramienta con una política propia pequeña. La frontera debe quedar clara para no duplicar la misma decisión en dos lugares.

La evaluación real requiere comparar alternativas actuales y costes completos. Este artículo no recomienda productos concretos ni afirma haber investigado sus prestaciones.

Soporte necesita responsables y operación

Una capacidad de soporte abandonada puede terminar consumiendo más tiempo que una inversión inicial suficiente.

Define quién responde por:

  • Cambios de reglas y su aprobación.
  • Revisión de casos derivados manualmente.
  • Registro y conservación de decisiones.
  • Errores operativos y tiempos de atención acordados.
  • Acceso a datos y permisos.
  • Pruebas y mantenimiento de dependencias.

No hace falta prometer niveles de servicio que no se han acordado. Sí hace falta que las excepciones tengan un destino real.

Una decisión MANUAL_REVIEW sin una bandeja o un responsable no resuelve el problema: solo lo traslada fuera del código.

Evitar que la devolución invada el dominio principal

Las recomendaciones pueden necesitar aprender que un producto se devuelve frecuentemente por incompatibilidad. Eso no significa que deban importar las reglas de admisión de devoluciones.

Si ese dato es necesario, se define qué información puede compartirse, con qué significado y bajo qué controles. El recomendador no necesita leer todas las notas privadas de los operadores.

De igual manera, modificar la priorización del recomendador no debería cambiar el plazo comercial de devolución por una dependencia accidental.

Mantener límites permite que cada capacidad evolucione por sus razones. La clasificación estratégica no reemplaza el diseño de esas relaciones.

Una capacidad de soporte puede cambiar de papel

Imagina que la tienda empieza a diferenciarse por recuperar productos, evaluar su estado y revenderlos con una experiencia excepcional.

La clasificación y disposición de devoluciones podría dejar de ser una tarea meramente operativa y convertirse en conocimiento estratégico de la nueva propuesta.

No bastaría con renombrar la carpeta. Habría que revisar inversión, datos, colaboración con especialistas y capacidades de experimentación.

La categoría se revisa cuando cambian estrategia o evidencia. No debe utilizarse para negar una mejora necesaria diciendo «siempre fue soporte».

El contraste en una plataforma educativa

En una plataforma centrada en práctica adaptativa, la gestión de solicitudes de cambio de grupo puede ser soporte: necesita reglas propias, pero no es la razón principal por la que el alumnado elige el producto.

Una implementación con validaciones, estados comprensibles y revisión de excepciones puede ser suficiente.

En otra organización que compite por recolocar estudiantes entre grupos compatibles y optimizar horarios personalizados, parte de esa misma capacidad podría ser central para su propuesta.

No cambia porque el código sea más largo. Cambia porque el resultado y el conocimiento especializado tienen otro papel estratégico.

Como tercer contraste, una empresa dedicada a gestionar operaciones administrativas para centros educativos puede encontrar diferenciación en automatizaciones que para esos centros son soporte. La clasificación depende de la organización que se analiza.

Errores frecuentes al implementar soporte

Todo debe ser CRUD. Si hay reglas y transiciones, esconderlas en controladores puede hacer el proceso más difícil de comprender.

Todo necesita un modelo complejo. Una política acotada puede expresarse con tipos, una función y pruebas antes de necesitar más abstracciones.

Soporte significa sin pruebas. Los errores de cantidades o decisiones operativas pueden ser críticos aunque no diferencien al negocio.

Automatizar cualquier excepción. Una revisión humana bien definida puede ser una primera solución más segura que reglas incompletas, siempre que tenga responsables y capacidad real.

Crear una herramienta universal. Diseñar para organizaciones imaginarias puede desviar esfuerzo del problema actual.

Dejar reglas en varios sitios. Si interfaz, automatización y backend aplican versiones distintas, la simplicidad aparente oculta una responsabilidad duplicada.

Usar IA para acelerar con un alcance claro

Para Cursor, Claude o Codex, describe reglas y límites antes de pedir una implementación.

Para elegir una solución:

Analiza estas reglas de recepción de devoluciones y el flujo de operadores. Propón la implementación más pequeña que conserve cantidades, permisos y trazabilidad. Distingue requisitos confirmados de preguntas pendientes. No añadas microservicios ni un motor universal sin una necesidad concreta.

Para implementar la política:

Implementa una función TypeScript con hechos de entrada y decisiones explícitas. Usa la política comercial ficticia adjunta; no inventes plazos legales. Prueba el instante límite, cantidades parciales y prioridad de revisión por daños. No implementes reembolsos ni supongas que la función resuelve concurrencia.

Para revisar:

Busca reglas duplicadas, unidades confiadas al cliente, estados sin responsable y abstracciones que no resuelven requisitos. Separa lo que cubren pruebas unitarias de lo que necesita persistencia y control concurrente. Explica qué corregir antes de automatizar una decisión.

Una persona debe validar la política, el alcance operativo y las obligaciones aplicables. El código generado puede acelerar una solución equivocada si esas decisiones faltan.

Ejercicios

1. El agregado por obligación

La política tiene tres decisiones estables. El equipo propone varios agregados y eventos distribuidos porque el proyecto usa DDD. ¿Qué justificación falta?

2. Dos solicitudes simultáneas

Queda una unidad admitible y llegan dos solicitudes de una unidad. Cada una pasa la función de evaluación con los mismos hechos. ¿La política está necesariamente mal?

3. El soporte que se vuelve propuesta

Una plataforma empieza a vender su capacidad de reasignar grupos y horarios como principal motivo para contratarla. ¿Qué debe revisarse?

Soluciones razonadas

1. Falta identificar qué regla, riesgo o necesidad operativa resuelve cada mecanismo. Utilizar DDD no obliga a implementar todos sus patrones. Una política pequeña y probada puede ser suficiente.

2. No necesariamente. La evaluación puede ser correcta para los hechos recibidos, pero falta coordinar su registro y actualización para que ambas solicitudes no consuman la misma unidad. Las pruebas de función no sustituyen ese control.

3. Su clasificación estratégica, el conocimiento que necesita el modelo y la inversión para mejorarlo. La capacidad puede pasar de soporte a diferenciadora; el cambio requiere evidencia y decisiones concretas, no solo otra etiqueta.

Sostener el negocio sin fabricar complejidad

Un Supporting Subdomain merece una solución que haga bien su trabajo, conserve sus reglas y pueda mantenerse con un coste razonable.

Empieza por las decisiones y los riesgos reales. Añade estructura cuando vuelva el proceso más claro o seguro, y conserva una vía de evolución cuando el negocio cambie.

La proporcionalidad se reconoce cuando puedes explicar qué necesidad resuelve cada parte y qué garantías siguen pendientes.

Cargando tus opciones de aprendizaje…

Cargando conversación…

Sigue aprendiendo