Aplicar Domain-Driven Design (DDD) en Microsoft Dynamics 365 Business Central SaaS no es un ejercicio académico ni una adaptación literal de patrones de otros ecosistemas como .NET puro o Java. Es un esfuerzo de reinterpretación. AL no es un lenguaje orientado a objetos completo, no tiene todas las abstracciones clásicas de DDD (como aggregates complejos con encapsulación total), y además está profundamente acoplado a un modelo de datos persistente y a procesos estándar del ERP.

Sin embargo, eso no significa que DDD no aplique. De hecho, en implementaciones complejas de Business Central, es precisamente el enfoque DDD el que permite evitar el caos estructural que aparece cuando la lógica de negocio se dispersa entre páginas, tablas, triggers, codeunits e integraciones sin una narrativa clara.

DDD en Business Central no se trata de replicar patrones de libro. Se trata de:

  • modelar correctamente el dominio de negocio
  • separar responsabilidades
  • evitar lógica dispersa
  • definir límites claros (bounded contexts)
  • construir soluciones que evolucionen sin romperse

El objetivo es que el código represente el negocio, no solo que funcione.

El problema Link to heading

El anti-pattern más común en extensiones de Business Central es el modelo anémico:

  • tablas con campos
  • triggers con validaciones mínimas
  • lógica distribuida en múltiples codeunits
  • procesos duplicados
  • ausencia de lenguaje ubicuo

Esto genera:

  • lógica inconsistente
  • dificultad de mantenimiento
  • bugs difíciles de rastrear
  • fuerte acoplamiento entre módulos

Otro problema es el acoplamiento implícito al modelo estándar. Muchas soluciones “se cuelgan” del comportamiento estándar sin definir su propio dominio, lo que produce dependencias frágiles.

DDD busca resolver esto estructurando el sistema alrededor del dominio real.

Conceptos clave adaptados a AL Link to heading

Entidades Link to heading

En AL, una entidad suele mapearse a una tabla.

Pero una tabla no es automáticamente una entidad de dominio.

Una entidad de dominio tiene:

  • identidad clara
  • reglas de negocio
  • invariantes

Ejemplo:

table 51000 "Loan Application"
{
    fields
    {
        field(1; "No."; Code[20]) { }
        field(2; "Customer No."; Code[20]) { }
        field(3; Amount; Decimal) { }
        field(4; Status; Enum "Loan Status") { }
    }
}

Pero la lógica NO debería vivir solo en la tabla.

Servicios de dominio Link to heading

La lógica de negocio debe encapsularse en codeunits.

codeunit 51001 "Loan Domain Service"
{
    procedure ApproveLoan(var Loan: Record "Loan Application")
    begin
        if Loan.Status <> Loan.Status::Pending then
            Error('Only pending loans can be approved.');

        ValidateLoanAmount(Loan);

        Loan.Status := Loan.Status::Approved;
        Loan.Modify(true);
    end;

    local procedure ValidateLoanAmount(var Loan: Record "Loan Application")
    begin
        if Loan.Amount <= 0 then
            Error('Loan amount must be greater than zero.');
    end;
}

Esto evita lógica dispersa.

Aggregates (adaptado) Link to heading

AL no soporta aggregates como en DDD puro, pero el patrón se puede aproximar.

Ejemplo:

  • Header = Aggregate root
  • Lines = internal entities
table 51010 "Order Header"
{
    fields
    {
        field(1; "No."; Code[20]) { }
    }
}
table 51011 "Order Line"
{
    fields
    {
        field(1; "Document No."; Code[20]) { }
        field(2; "Line No."; Integer) { }
    }
}

La lógica debe ejecutarse desde el root.

Value Objects (simulación) Link to heading

AL no tiene value objects nativos, pero se pueden simular con:

  • records temporales
  • structs JSON
  • codeunits

Ejemplo:

codeunit 51002 "Money VO"
{
    procedure Create(Amount: Decimal; Currency: Code[10]): Text
    begin
        exit(StrSubstNo('%1|%2', Amount, Currency));
    end;
}

No es perfecto, pero sirve para encapsular intención.

Bounded Contexts Link to heading

Una extensión compleja debería separarse por contexto.

Ejemplo:

  • Sales Context
  • Finance Context
  • Integration Context

Cada uno con:

  • sus tablas
  • sus codeunits
  • sus reglas

Evitar compartir lógica indiscriminadamente.

Diseño de la solución Link to heading

1. Separar capas Link to heading

Incluso en AL, se puede estructurar:

  • dominio (rules)
  • aplicación (orquestación)
  • infraestructura (HTTP, logs, colas)

2. Usar lenguaje ubicuo Link to heading

Nombres deben reflejar negocio:

Mal:

  • Utils
  • Manager

Bien:

  • Loan Approval Service
  • Payment Validation Engine

3. Evitar lógica en páginas Link to heading

Las páginas NO son dominio.

4. Reducir lógica en triggers Link to heading

Triggers deben validar invariantes simples, no procesos completos.

Ejemplo completo Link to heading

Orquestador:

codeunit 51003 "Loan Application Service"
{
    procedure ProcessLoan(var Loan: Record "Loan Application")
    var
        DomainService: Codeunit "Loan Domain Service";
    begin
        ValidateLoan(Loan);
        DomainService.ApproveLoan(Loan);
    end;

    local procedure ValidateLoan(var Loan: Record "Loan Application")
    begin
        if Loan."Customer No." = '' then
            Error('Customer is required.');
    end;
}

Integración con eventos Link to heading

DDD + Event Driven es potente.

[IntegrationEvent(false, false)]
procedure OnLoanApproved(LoanNo: Code[20])
begin
end;

Esto desacopla procesos.

Anti-patterns Link to heading

  • lógica distribuida sin estructura
  • codeunits monolíticas
  • tablas sin intención de dominio
  • abuso de triggers
  • falta de naming semántico

Buenas prácticas Link to heading

  • modelar el dominio primero
  • encapsular reglas en codeunits
  • separar responsabilidades
  • usar eventos para desacoplar
  • pensar en evolución

Conclusiones Link to heading

Implementar DDD en Business Central no es copiar patrones de otros ecosistemas, sino adaptar principios al modelo AL. Cuando se hace correctamente, el código deja de ser una colección de soluciones puntuales y pasa a ser un reflejo estructurado del negocio.

Esto reduce complejidad, mejora mantenibilidad y permite escalar la solución sin degradación progresiva. En entornos empresariales reales, esta diferencia es crítica.