Diseño de Commands
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:
-
Imperativo: El nombre debe ser una orden en infinitivo:
CreateOrder,CancelSubscription,UpdateProfile. Evita nombres comoOrderCreationoOrderToCreateque suenan como sustantivos. El comando expresa intencion de accion. -
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. -
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).
-
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:
- commandId: Identificador unico del comando. Permite rastrear este comando especifico en logs y metricas.
- timestamp: Momento en que se creo el comando. Util para auditorias y ordenamiento.
- correlationId: Identificador opcional que agrupa comandos relacionados. Por ejemplo, todos los comandos originados por una misma solicitud HTTP.
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
-
Un comando = una intencion: Cada comando debe representar una sola accion de negocio. Si necesitas hacer dos cosas, usa dos comandos.
-
Nombres descriptivos: Usa verbos especificos.
ConfirmOrderes mejor queProcessOrderporque indica exactamente que sucede. -
Datos minimos: Incluye solo lo necesario para la accion. No agregues campos “por si acaso”.
-
IDs, no entidades: Pasa
customerId, no el objetoCustomer. El handler obtendra la entidad si la necesita. -
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.