Magento 2 Command pattern with Message Queue combines two mechanisms: Command encapsulates an operation as an object, and Message Queue executes it asynchronously. In practice: instead of sending an email synchronously in an HTTP request (blocking the response), you push a message to a queue and a consumer handles it in the background. I show how to implement this using order notifications as an example.
Command pattern – basics
<?php
declare(strict_types=1);
interface CommandInterface
{
public function execute(): void;
}
class SendOrderConfirmationCommand implements CommandInterface
{
public function __construct(
private int $orderId,
private string $customerEmail,
private string $customerName,
) {}
public function execute(): void
{
echo "Sending email to {$this->customerEmail} for order #{$this->orderId}\n";
}
public function getOrderId(): int { return $this->orderId; }
public function getCustomerEmail(): string { return $this->customerEmail; }
public function getCustomerName(): string { return $this->customerName; }
}
class CommandBus
{
private array $handlers = [];
public function register(string $commandClass, callable $handler): void
{
$this->handlers[$commandClass] = $handler;
}
public function dispatch(CommandInterface $command): void
{
$class = get_class($command);
if (!isset($this->handlers[$class])) {
throw new \RuntimeException("No handler for {$class}");
}
($this->handlers[$class])($command);
}
}
Message Queue in Magento 2 – configuration
<!-- etc/communication.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd">
<topic name="vendor.module.order.notification"
is_synchronous="false"
request="Vendor\Module\Api\Data\OrderNotificationInterface"/>
</config>
<!-- etc/queue_consumer.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd">
<consumer name="vendor.module.order.notification"
queue="vendor.module.order.notification"
connection="amqp"
maxMessages="100"
handler="Vendor\Module\Model\Queue\OrderNotificationConsumer::process"/>
</config>
Publisher and Consumer
<?php
declare(strict_types=1);
// Publisher - pushes message to queue
class OrderNotificationPublisher
{
private const TOPIC = 'vendor.module.order.notification';
public function __construct(
private \Magento\Framework\MessageQueue\PublisherInterface $publisher,
private \Vendor\Module\Api\Data\OrderNotificationInterfaceFactory $factory,
) {}
public function publish(int $orderId, string $email, string $name): void
{
$notification = $this->factory->create();
$notification->setOrderId($orderId);
$notification->setCustomerEmail($email);
$notification->setCustomerName($name);
// Asynchronous - method returns immediately
$this->publisher->publish(self::TOPIC, $notification);
}
}
// Consumer - processes messages from queue (started via CLI)
class OrderNotificationConsumer
{
public function __construct(
private \Vendor\Module\Service\EmailService $emailService,
private \Psr\Log\LoggerInterface $logger,
) {}
public function process(\Vendor\Module\Api\Data\OrderNotificationInterface $notification): void
{
try {
$this->emailService->sendOrderConfirmation(
$notification->getCustomerEmail(),
$notification->getCustomerName(),
$notification->getOrderId(),
);
} catch (\Exception $e) {
$this->logger->error('Order notification failed', [
'order_id' => $notification->getOrderId(),
'error' => $e->getMessage(),
]);
throw $e; // retry
}
}
}
Starting the consumer
bin/magento queue:consumers:start vendor.module.order.notification bin/magento queue:consumers:start vendor.module.order.notification --max-messages=100 bin/magento queue:consumers:list # In production - run via supervisor: # [program:magento-consumer] # command=/usr/bin/php /var/www/html/bin/magento queue:consumers:start vendor.module.order.notification # autostart=true # autorestart=true # numprocs=2
Summary
Command + Message Queue is the pattern for operations that: take a long time (email, PDF, external API integration), can fail and need retrying, or can be executed asynchronously without affecting user UX. Magento 2 supports both MySQL (for simple cases without RabbitMQ) and AMQP (RabbitMQ) as queue backends. For most shops MySQL queue is enough; AMQP is needed at high message volume.
