Arquitectura BLoC en Apps de Retail: Por Qué Tu Flujo de Checkout con 7 BLoCs Está Matando Tu App

Alejandro Arciniegas

aug 20, 2025

Muy bien, hablemos de algo que he tenido en mente últimamente. Estuve revisando una propuesta de arquitectura de checkout para una app de retail importante, y vi algo que me recordó a errores que cometí al inicio de mi carrera. Siete BLoCs. SIETE. Para un solo flujo de checkout. Cada uno comunicándose con los otros a través de eventos en lo que parecía un sistema distribuido complejo.

Mira, lo entiendo. Todos hemos pasado por eso. Lees sobre el patrón BLoC, Clean Architecture, y de repente todo necesita su propio BLoC. Yo hice lo mismo cuando aprendí estos patrones por primera vez. Pero después de construir flujos de checkout para Walmart Glass y trabajar en los sistemas de pago de My Firestone, he aprendido algunas lecciones difíciles sobre cuándo este enfoque funciona y cuándo no.

La Arquitectura Que Me Quitó el Sueño

Esto es lo que estaba viendo:

// Esto es lo que NO debes hacer
class BlocCheckoutCart extends Bloc<CartEvent, CartState> {
  final BlocCheckoutDelivery deliveryBloc;  // Bandera roja #1
  final BlocCheckoutPago paymentBloc;        // Bandera roja #2
  
  BlocCheckoutCart() {
    // ¿BLoCs escuchando a otros BLoCs? Bienvenido al infierno
    deliveryBloc.stream.listen((state) {
      add(UpdateCartBasedOnDelivery(state));
    });
  }
}

Siete BLoCs diferentes, todos comunicándose a través de eventos: BlocCheckoutCart, BlocCheckoutDelivery, BlocCheckoutPago, BlocCheckoutCupones, BlocCheckoutValidation, y un par más.

Reconocí este patrón inmediatamente porque he construido sistemas así antes. Aunque la intención es buena - seguir los principios de Clean Architecture - este enfoque a menudo lleva a una complejidad arquitectónica que es difícil de mantener y debuggear.

Por Qué la Comunicación Directa Entre BLoCs Crea Desafíos

Desde mi experiencia, tener BLoCs que se comunican directamente a través de eventos crea varios desafíos arquitectónicos:

1. Las Dependencias Circulares Son Tu Enemigo

Cuando BLoC A escucha a BLoC B, y BLoC B escucha a BLoC A, has creado un ciclo de dependencias que se vuelve cada vez más difícil de debuggear y mantener. He pasado muchas noches tardías debuggeando estos escenarios, y he aprendido a evitarlos.

// NO HAGAS ESTO - Esto crea dependencias circulares imposibles de debuggear
class PaymentBloc extends Bloc {
  PaymentBloc(this.cartBloc) {
    cartBloc.stream.listen((state) {
      // Ahora payment depende de cart...
    });
  }
}

class CartBloc extends Bloc {
  CartBloc(this.paymentBloc) {
    paymentBloc.stream.listen((state) {
      // ...y cart depende de payment. ¡Esto crea un desafío de debugging!
    });
  }
}

2. El Estado Se Vuelve Caos Impredecible

Con múltiples BLoCs disparando eventos entre sí, el estado de tu app se convierte en la combinación de todos los estados individuales. Trata de explicarle a tu PM por qué el botón de checkout está deshabilitado cuando el carrito tiene items, el delivery está seleccionado, y el pago es válido. Spoiler: es porque el BLoC #5 todavía está procesando un evento del BLoC #3.

3. ¿Testing? Olvídalo

Escribir tests unitarios para un solo BLoC es sencillo. Sin embargo, testear siete BLoCs interdependientes que necesitan ser mockeados, stubbeados y orquestados juntos se vuelve exponencialmente más complejo, especialmente para operaciones simples como agregar un item al carrito.

La Forma Correcta: Arquitectura Event-Driven con un Orquestador

Después de años de construir apps de retail que realmente escalan, este es el patrón que funciona. Y sí, todavía usa BLoC y sigue los principios de Clean Architecture, pero sin la locura.

Fuente Única de Verdad: El Patrón Orquestador

// UN BLoC para gobernarlos a todos
class CheckoutOrchestrator extends Bloc<CheckoutEvent, CheckoutState> {
  final CheckoutRepository _repository;
  final EventBus _eventBus;
  
  CheckoutOrchestrator(this._repository, this._eventBus) 
    : super(CheckoutState.initial()) {
    
    // Maneja todos los eventos de checkout en un lugar
    on<AddItemToCart>(_handleAddItem);
    on<SelectDeliveryMethod>(_handleDeliverySelection);
    on<ProcessPayment>(_handlePayment);
    
    // Los servicios se comunican a través del event bus, no referencias directas
    _eventBus.on<ServiceEvent>().listen(_handleServiceEvent);
  }
  
  // Estado único e inmutable
  @override
  CheckoutState get state => CheckoutState(
    cart: _cartData,
    delivery: _deliveryData,
    payment: _paymentData,
    currentStep: _currentStep,
    validation: _validationResult,
  );
}

Esto es similar a lo que implementamos en Walmart para su función Scan & Go. Un orquestador, flujo claro, sin dependencias circulares.

Servicios Desacoplados Que Se Ocupan de Sus Propios Asuntos

En lugar de BLoCs hablando entre sí, usa servicios especializados que manejan lógica de negocio específica:

// Los servicios son pura lógica de negocio, sin preocupaciones de UI
abstract class CheckoutService {
  Stream<ServiceEvent> process(CheckoutData data);
}

class DeliveryService implements CheckoutService {
  @override
  Stream<ServiceEvent> process(CheckoutData data) async* {
    // Calcula opciones de delivery basado en el carrito
    final options = await _calculateDeliveryOptions(data.cart);
    yield DeliveryOptionsCalculated(options);
    
    // Valida dirección si es necesario
    if (data.delivery?.address != null) {
      final validation = await _validateAddress(data.delivery.address);
      yield AddressValidated(validation);
    }
  }
}

class PaymentService implements CheckoutService {
  @override
  Stream<ServiceEvent> process(CheckoutData data) async* {
    // Procesa pago independientemente
    // Sin conocimiento de delivery o cart BLoCs
    final result = await _processPayment(data.payment);
    yield PaymentProcessed(result);
  }
}

Patrón Repository con Strategy para Métodos de Pago

Aquí es donde se pone hermoso. En lugar de tener diferentes BLoCs para diferentes métodos de pago, usa el patrón Strategy:

class CheckoutRepository {
  final Map<PaymentMethod, PaymentStrategy> _strategies = {
    PaymentMethod.creditCard: CreditCardStrategy(),
    PaymentMethod.paypal: PayPalStrategy(),
    PaymentMethod.applePay: ApplePayStrategy(),
    PaymentMethod.giftCard: GiftCardStrategy(),
  };
  
  Future<CheckoutResult> processCheckout(CheckoutData data) async {
    // Valida todo en un pipeline
    final validation = await _validationPipeline.validate(data);
    if (!validation.isValid) {
      return CheckoutResult.validationFailed(validation);
    }
    
    // Procesa pago con la estrategia apropiada
    final strategy = _strategies[data.payment.method];
    return strategy.execute(data);
  }
}

Arquitectura Real Que Realmente Funciona

Este es el diagrama de arquitectura que no te hará querer renunciar al desarrollo de software:

┌─────────────────────┐
   CheckoutPage      
└──────────┬──────────┘
           
    ┌──────▼──────┐
     Orchestrator│  BLoC único manejando el flujo
    └──────┬──────┘
           
    ┌──────▼──────────┐
       Event Bus       Comunicación desacoplada
    └────┬─────┬──────┘
              
    ┌────▼─┐ ┌─▼──────┐
    │Service││Repository│  Unidades independientes y testeables
    └──────┘ └─────────┘

¿Notas lo que falta? BLoCs hablando con BLoCs. Servicios dependiendo unos de otros. Dependencias circulares. Eso no es accidente.

El Pipeline de Validación Que Realmente Escala

Uno de los errores más grandes que veo es esparcir la lógica de validación a través de múltiples BLoCs. Así es como hacerlo bien:

class ValidationPipeline {
  final List<Validator> validators = [
    InventoryValidator(),     // Chequea disponibilidad de stock
    FraudValidator(),         // Detección de fraude
    PaymentValidator(),       // Validación de método de pago
    DeliveryValidator(),      // Restricciones de delivery
    BusinessRulesValidator(), // Reglas de negocio personalizadas
  ];
  
  Future<ValidationResult> validate(CheckoutData data) async {
    final results = <ValidationResult>[];
    
    // Ejecuta validadores en paralelo cuando sea posible
    final futures = validators.map((v) => v.validate(data));
    results.addAll(await Future.wait(futures));
    
    // Combina resultados
    final errors = results
        .where((r) => !r.isValid)
        .expand((r) => r.errors)
        .toList();
        
    return errors.isEmpty 
        ? ValidationResult.success()
        : ValidationResult.failure(errors);
  }
}

Este enfoque te permite agregar nuevos validadores sin tocar código existente. Esos son los principios SOLID en acción, no siete BLoCs jugando teléfono descompuesto.

Manejo de Estado Que No Apesta

Usa freezed o built_value para estado inmutable. Confía en mí:

@freezed
class CheckoutState with _$CheckoutState {
  const factory CheckoutState({
    required CheckoutStep currentStep,
    required CartSummary cart,
    DeliveryInfo? delivery,
    PaymentInfo? payment,
    ValidationResult? validation,
    @Default(false) bool isProcessing,
    @Default(false) bool isComplete,
    CheckoutError? error,
  }) = _CheckoutState;
  
  // Métodos helper para transiciones de estado
  const CheckoutState._();
  
  bool get canProceedToDelivery => 
      cart.items.isNotEmpty && !cart.hasUnavailableItems;
      
  bool get canProceedToPayment => 
      canProceedToDelivery && delivery != null;
      
  bool get canCompleteCheckout => 
      canProceedToPayment && 
      payment != null && 
      validation?.isValid == true;
}

Optimizaciones de Performance Que Importan

Después de implementar este patrón a escala, estas son las victorias de performance:

  1. BLoC Único = Menos Rebuilds: En lugar de siete BLoCs causando rebuilds, tienes una fuente controlada
  2. Event Bus = Procesamiento Async: Los servicios pueden procesar en paralelo sin bloquear la UI
  3. Estado Inmutable = Updates Predecibles: No hay mutaciones misteriosas de estado de BLoCs aleatorios

Las Lecciones Que Me Costaron Sueño

Después de construir flujos de checkout que procesan millones de transacciones, esto es lo que sé con certeza:

  1. Empieza Simple: Comienza con un BLoC. Agrega complejidad solo cuando tengas métricas que prueben que la necesitas
  2. Servicios Sobre BLoCs: La lógica de negocio pertenece en servicios, no en manejo de estado de UI
  3. Los Eventos No Son Tu Message Bus: Los eventos de BLoC son para interacciones de usuario, no comunicación inter-componente
  4. Testea el Flujo, No la Implementación: Tus tests deberían validar resultados de negocio, no interacciones de BLoC

Cuándo Realmente Necesitas Múltiples BLoCs

Seamos realistas - a veces sí necesitas múltiples BLoCs. Pero aquí es cuándo:

  • Funcionalidades Completamente Independientes: El perfil de usuario y checkout pueden tener BLoCs separados
  • Diferentes Ciclos de Vida: Una función de chat que persiste mientras navegas puede tener su propio BLoC
  • Límites de Módulo: Si estás usando arquitectura modular, cada módulo puede tener su BLoC

Pero aún entonces, NUNCA deberían comunicarse directamente a través de eventos. Usa un repository compartido o capa de servicio en su lugar.

La Línea de Fondo

Mira, he estado donde tú estás. Quieres hacer las cosas "bien". Quieres Clean Architecture. Quieres que tu código sea "enterprise-ready". Pero aquí está la verdad: la mejor arquitectura es la más simple que resuelve tu problema.

En Walmart, manejamos el tráfico de Black Friday con este patrón. En Firestone, procesamos miles de citas de servicio. No con siete BLoCs jugando papa caliente con eventos, sino con un orquestador único y bien diseñado y servicios desacoplados.

Así que antes de crear ese BlocCheckoutStepValidatorListenerObserver, pregúntate: "¿Estoy resolviendo un problema real, o estoy agregando complejidad innecesaria?"

Tu yo futuro (y tu equipo) te lo agradecerán.

¿Quieres Aprender Más?

Si estás lidiando con manejo de estado complejo en Flutter o necesitas ayuda arquitecturando tu app de retail, no dudes en contactarme. He pasado por las trincheras y estoy feliz de ayudarte a evitar las mismas trampas que he encontrado.

Recuerda: La buena arquitectura es invisible. Si constantemente estás pensando en tu arquitectura mientras agregas funcionalidades, lo estás haciendo mal.