← Volver al listado de tecnologías

Diseño de Commands

Por: SiempreListo
cqrscommandsdddtypescriptgopython

Diseno de Commands

En este capitulo aprenderemos a disenar Commands efectivos: como estructurarlos, que informacion deben contener, y cuales son las mejores practicas para su implementacion.

Que es un Command

Un Command (comando) es un objeto que representa una intencion de cambiar el estado del sistema. Piensa en el como un mensaje que dice “quiero que hagas esto”. El comando no ejecuta la accion por si mismo; simplemente transporta toda la informacion necesaria para que un handler pueda ejecutarla.

Los commands son objetos inmutables: una vez creados, sus datos no pueden cambiar. Esto garantiza que el comando que se envio es exactamente el mismo que se procesa, sin modificaciones accidentales en el camino.

Caracteristicas Fundamentales de un Command

Un command bien disenado tiene cuatro caracteristicas esenciales:

  1. Imperativo: El nombre debe ser una orden en infinitivo: CreateOrder, CancelSubscription, UpdateProfile. Evita nombres como OrderCreation o OrderToCreate que suenan como sustantivos. El comando expresa intencion de accion.

  2. Inmutable: Una vez creado, no puede cambiar. Todos sus campos son de solo lectura (readonly). Esto previene bugs donde el comando se modifica accidentalmente durante su procesamiento.

  3. Autocontenido: Incluye toda la informacion necesaria para ejecutar la accion. El handler no deberia necesitar buscar datos adicionales para procesar el comando (salvo validaciones contra la base de datos).

  4. Sin retorno significativo: En teoria pura, un comando no devuelve datos. En la practica, es aceptable retornar un ID generado o un estado de confirmacion, pero nunca entidades completas o listas de datos.

Estructura Base de un Command

Todo command comparte ciertos campos comunes que facilitan su trazabilidad y depuracion:

Veamos como implementar esta estructura base:

// TypeScript - Interfaz base
interface Command {
  readonly commandId: string;
  readonly timestamp: Date;
  readonly correlationId?: string;
}

interface CommandHandler<T extends Command> {
  execute(command: T): Promise<void>;
}
// Go - Interfaz base
type Command interface {
    CommandID() string
    Timestamp() time.Time
    CorrelationID() string
}

type CommandHandler[T Command] interface {
    Execute(ctx context.Context, cmd T) error
}
# Python - Clase base
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from uuid import uuid4

@dataclass(frozen=True)
class Command(ABC):
    command_id: str = field(default_factory=lambda: str(uuid4()))
    timestamp: datetime = field(default_factory=datetime.utcnow)
    correlation_id: str | None = None

class CommandHandler[T: Command](ABC):
    @abstractmethod
    async def execute(self, command: T) -> None: ...

Commands de OrderFlow: Ejemplos Practicos

Veamos commands concretos para nuestro sistema de pedidos. Cada uno representa una accion de negocio especifica.

CreateOrder: Crear un Nuevo Pedido

Este comando inicia un nuevo pedido. Nota que usamos DTOs (Data Transfer Objects) para los items y la direccion, estructuras simples que solo transportan datos:

// TypeScript
@dataclass
class CreateOrderCommand implements Command {
  readonly commandId = crypto.randomUUID();
  readonly timestamp = new Date();

  constructor(
    readonly customerId: string,
    readonly items: readonly OrderItemDTO[],
    readonly shippingAddress: AddressDTO,
    readonly correlationId?: string
  ) {}
}

interface OrderItemDTO {
  readonly productId: string;
  readonly quantity: number;
}
// Go
type CreateOrderCommand struct {
    commandID       string
    timestamp       time.Time
    correlationID   string
    CustomerID      string
    Items           []OrderItemDTO
    ShippingAddress AddressDTO
}

func NewCreateOrderCommand(
    customerID string,
    items []OrderItemDTO,
    address AddressDTO,
) CreateOrderCommand {
    return CreateOrderCommand{
        commandID:       uuid.NewString(),
        timestamp:       time.Now(),
        CustomerID:      customerID,
        Items:           items,
        ShippingAddress: address,
    }
}

func (c CreateOrderCommand) CommandID() string     { return c.commandID }
func (c CreateOrderCommand) Timestamp() time.Time  { return c.timestamp }
func (c CreateOrderCommand) CorrelationID() string { return c.correlationID }
# Python
@dataclass(frozen=True)
class CreateOrderCommand(Command):
    customer_id: str
    items: tuple[OrderItemDTO, ...]
    shipping_address: AddressDTO

AddItemToOrder: Agregar Producto a un Pedido Existente

Este comando modifica un pedido existente. Nota que pasa el ID del pedido, no el objeto pedido completo. Esto es importante: los commands trabajan con identificadores, no con entidades:

// TypeScript
class AddItemToOrderCommand implements Command {
  readonly commandId = crypto.randomUUID();
  readonly timestamp = new Date();

  constructor(
    readonly orderId: string,
    readonly productId: string,
    readonly quantity: number
  ) {}
}
// Go
type AddItemToOrderCommand struct {
    commandID string
    timestamp time.Time
    OrderID   string
    ProductID string
    Quantity  int
}

func NewAddItemToOrderCommand(orderID, productID string, qty int) AddItemToOrderCommand {
    return AddItemToOrderCommand{
        commandID: uuid.NewString(),
        timestamp: time.Now(),
        OrderID:   orderID,
        ProductID: productID,
        Quantity:  qty,
    }
}
# Python
@dataclass(frozen=True)
class AddItemToOrderCommand(Command):
    order_id: str
    product_id: str
    quantity: int

ConfirmOrder: Confirmar y Procesar el Pago

Este comando indica que el cliente desea proceder con la compra. Incluye el metodo de pago seleccionado:

// TypeScript
class ConfirmOrderCommand implements Command {
  readonly commandId = crypto.randomUUID();
  readonly timestamp = new Date();

  constructor(
    readonly orderId: string,
    readonly paymentMethodId: string
  ) {}
}
// Go
type ConfirmOrderCommand struct {
    commandID       string
    timestamp       time.Time
    OrderID         string
    PaymentMethodID string
}
# Python
@dataclass(frozen=True)
class ConfirmOrderCommand(Command):
    order_id: str
    payment_method_id: str

CancelOrder: Cancelar un Pedido

Para cancelaciones, incluimos siempre una razon. Esto es util para analisis de negocio y auditorias:

// TypeScript
class CancelOrderCommand implements Command {
  readonly commandId = crypto.randomUUID();
  readonly timestamp = new Date();

  constructor(
    readonly orderId: string,
    readonly reason: string
  ) {}
}
// Go
type CancelOrderCommand struct {
    commandID string
    timestamp time.Time
    OrderID   string
    Reason    string
}
# Python
@dataclass(frozen=True)
class CancelOrderCommand(Command):
    order_id: str
    reason: str

Organizacion de Commands en el Proyecto

Es importante mantener los commands organizados. Una estructura comun es agruparlos por entidad de dominio:

src/
  application/
    commands/
      order/
        create-order.command.ts
        add-item.command.ts
        confirm-order.command.ts
        cancel-order.command.ts
      product/
        create-product.command.ts
        update-stock.command.ts

Buenas Practicas para Commands

  1. Un comando = una intencion: Cada comando debe representar una sola accion de negocio. Si necesitas hacer dos cosas, usa dos comandos.

  2. Nombres descriptivos: Usa verbos especificos. ConfirmOrder es mejor que ProcessOrder porque indica exactamente que sucede.

  3. Datos minimos: Incluye solo lo necesario para la accion. No agregues campos “por si acaso”.

  4. IDs, no entidades: Pasa customerId, no el objeto Customer. El handler obtendra la entidad si la necesita.

  5. Validacion basica en construccion: Verifica que los campos requeridos esten presentes al crear el comando. La validacion de negocio ocurre en el handler.

Proximos Pasos

En el siguiente capitulo disenaremos las Queries y Read Models para consultas optimizadas.


Glosario

Command (Comando)

Definicion: Objeto inmutable que representa una intencion de modificar el estado del sistema. Contiene toda la informacion necesaria para ejecutar una accion especifica.

Por que es importante: Desacopla la solicitud de cambio de su ejecucion. Permite validar, serializar, encolar y auditar las intenciones de modificacion.

Ejemplo practico: CreateOrderCommand contiene customerId, items y shippingAddress. El handler recibe este comando y crea el pedido usando esos datos.


Inmutabilidad

Definicion: Propiedad de un objeto que no puede ser modificado despues de su creacion. Todos sus campos son de solo lectura.

Por que es importante: Previene efectos secundarios inesperados. Si un objeto no puede cambiar, puedes pasarlo a cualquier funcion sin temor a que lo modifique.

Ejemplo practico: Un CreateOrderCommand usa readonly en todos sus campos. Una vez creado con customerId “123”, ese valor no puede cambiar.


DTO (Data Transfer Object)

Definicion: Objeto simple que solo transporta datos entre procesos o capas. No contiene logica de negocio, solo campos de datos.

Por que es importante: Separa la representacion de datos de la logica de negocio. Permite definir estructuras especificas para transferencia sin contaminar las entidades de dominio.

Ejemplo practico: OrderItemDTO contiene solo productId y quantity. No tiene metodos para calcular subtotales ni validar stock; eso lo hace el agregado.


Command Handler (Manejador de Comando)

Definicion: Componente que recibe un comando especifico y ejecuta la logica necesaria para procesarlo. Cada tipo de comando tiene su propio handler dedicado.

Por que es importante: Aisla la logica de procesamiento de cada accion en una clase especializada. Facilita el testing y la modificacion independiente de cada operacion.

Ejemplo practico: CreateOrderHandler recibe CreateOrderCommand, crea el agregado Order, lo persiste y publica los eventos resultantes.


Correlation ID

Definicion: Identificador que agrupa multiples comandos, eventos o mensajes que pertenecen a una misma operacion o flujo de negocio.

Por que es importante: Permite rastrear una solicitud a traves de multiples servicios y operaciones. Esencial para debugging y monitoreo en sistemas distribuidos.

Ejemplo practico: Una solicitud HTTP genera un correlationId. El CreateOrderCommand y todos los eventos que produce (OrderCreatedEvent, InventoryReservedEvent) comparten ese ID, permitiendo seguir el flujo completo.