diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..bdf2dcb
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+/Tests export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+.travis.yml export-ignore
+phpunit.xml.dist export-ignore
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..28a46e9
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,30 @@
+name: CI
+on:
+  pull_request:
+  push:
+    branches:
+      - master
+jobs:
+  tests:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        php: ['7.4', '8.0', '8.1', '8.2']
+
+    name: PHP ${{ matrix.php }} tests
+
+    steps:
+      - uses: actions/checkout@v2
+
+      - uses: shivammathur/setup-php@v2
+        with:
+          php-version: ${{ matrix.php }}
+          coverage: none
+          extensions: mongodb
+
+      - run: php Tests/fix_composer_json.php
+
+      - uses: "ramsey/composer-install@v1"
+
+      - run: vendor/bin/phpunit --exclude-group=functional
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 658dcab..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-sudo: false
-
-git:
-    depth: 10
-
-language: php
-
-php:
-    - '5.6'
-    - '7.0'
-
-cache:
-    directories:
-        - $HOME/.composer/cache
-
-install:
-    - php Tests/fix_composer_json.php
-    - composer self-update
-    - composer install
-
-script:
-    - vendor/bin/phpunit --exclude-group=functional
diff --git a/Client/ArrayProcessorRegistry.php b/ArrayProcessorRegistry.php
similarity index 55%
rename from Client/ArrayProcessorRegistry.php
rename to ArrayProcessorRegistry.php
index d6cb300..592908c 100644
--- a/Client/ArrayProcessorRegistry.php
+++ b/ArrayProcessorRegistry.php
@@ -1,37 +1,33 @@
 <?php
 
-namespace Enqueue\Client;
+namespace Enqueue;
 
-use Interop\Queue\PsrProcessor;
+use Interop\Queue\Processor;
 
 class ArrayProcessorRegistry implements ProcessorRegistryInterface
 {
     /**
-     * @var PsrProcessor[]
+     * @var Processor[]
      */
     private $processors;
 
     /**
-     * @param PsrProcessor[] $processors
+     * @param Processor[] $processors
      */
     public function __construct(array $processors = [])
     {
-        $this->processors = $processors;
+        $this->processors = [];
+        array_walk($processors, function (Processor $processor, string $key) {
+            $this->processors[$key] = $processor;
+        });
     }
 
-    /**
-     * @param string       $name
-     * @param PsrProcessor $processor
-     */
-    public function add($name, PsrProcessor $processor)
+    public function add(string $name, Processor $processor): void
     {
         $this->processors[$name] = $processor;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function get($processorName)
+    public function get(string $processorName): Processor
     {
         if (false == isset($this->processors[$processorName])) {
             throw new \LogicException(sprintf('Processor was not found. processorName: "%s"', $processorName));
diff --git a/Client/Amqp/AmqpDriver.php b/Client/Amqp/AmqpDriver.php
deleted file mode 100644
index e8b5871..0000000
--- a/Client/Amqp/AmqpDriver.php
+++ /dev/null
@@ -1,205 +0,0 @@
-<?php
-
-namespace  Enqueue\Client\Amqp;
-
-use Enqueue\Client\Config;
-use Enqueue\Client\DriverInterface;
-use Enqueue\Client\Message;
-use Enqueue\Client\Meta\QueueMetaRegistry;
-use Interop\Amqp\AmqpContext;
-use Interop\Amqp\AmqpMessage;
-use Interop\Amqp\AmqpQueue;
-use Interop\Amqp\AmqpTopic;
-use Interop\Amqp\Impl\AmqpBind;
-use Interop\Queue\PsrMessage;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-
-class AmqpDriver implements DriverInterface
-{
-    /**
-     * @var AmqpContext
-     */
-    private $context;
-
-    /**
-     * @var Config
-     */
-    private $config;
-
-    /**
-     * @var QueueMetaRegistry
-     */
-    private $queueMetaRegistry;
-
-    /**
-     * @param AmqpContext       $context
-     * @param Config            $config
-     * @param QueueMetaRegistry $queueMetaRegistry
-     */
-    public function __construct(AmqpContext $context, Config $config, QueueMetaRegistry $queueMetaRegistry)
-    {
-        $this->context = $context;
-        $this->config = $config;
-        $this->queueMetaRegistry = $queueMetaRegistry;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function sendToRouter(Message $message)
-    {
-        if (false == $message->getProperty(Config::PARAMETER_TOPIC_NAME)) {
-            throw new \LogicException('Topic name parameter is required but is not set');
-        }
-
-        $topic = $this->createRouterTopic();
-        $transportMessage = $this->createTransportMessage($message);
-
-        $this->context->createProducer()->send($topic, $transportMessage);
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function sendToProcessor(Message $message)
-    {
-        if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) {
-            throw new \LogicException('Processor name parameter is required but is not set');
-        }
-
-        if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) {
-            throw new \LogicException('Queue name parameter is required but is not set');
-        }
-
-        $transportMessage = $this->createTransportMessage($message);
-        $destination = $this->createQueue($queueName);
-
-        $this->context->createProducer()->send($destination, $transportMessage);
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function setupBroker(LoggerInterface $logger = null)
-    {
-        $logger = $logger ?: new NullLogger();
-        $log = function ($text, ...$args) use ($logger) {
-            $logger->debug(sprintf('[AmqpDriver] '.$text, ...$args));
-        };
-
-        // setup router
-        $routerTopic = $this->createRouterTopic();
-        $routerQueue = $this->createQueue($this->config->getRouterQueueName());
-
-        $log('Declare router exchange: %s', $routerTopic->getTopicName());
-        $this->context->declareTopic($routerTopic);
-        $log('Declare router queue: %s', $routerQueue->getQueueName());
-        $this->context->declareQueue($routerQueue);
-        $log('Bind router queue to exchange: %s -> %s', $routerQueue->getQueueName(), $routerTopic->getTopicName());
-        $this->context->bind(new AmqpBind($routerTopic, $routerQueue, $routerQueue->getQueueName()));
-
-        // setup queues
-        foreach ($this->queueMetaRegistry->getQueuesMeta() as $meta) {
-            $queue = $this->createQueue($meta->getClientName());
-
-            $log('Declare processor queue: %s', $queue->getQueueName());
-            $this->context->declareQueue($queue);
-        }
-    }
-
-    /**
-     * {@inheritdoc}
-     *
-     * @return AmqpQueue
-     */
-    public function createQueue($queueName)
-    {
-        $transportName = $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName();
-
-        $queue = $this->context->createQueue($transportName);
-        $queue->addFlag(AmqpQueue::FLAG_DURABLE);
-
-        return $queue;
-    }
-
-    /**
-     * {@inheritdoc}
-     *
-     * @return AmqpMessage
-     */
-    public function createTransportMessage(Message $message)
-    {
-        $headers = $message->getHeaders();
-        $properties = $message->getProperties();
-
-        $transportMessage = $this->context->createMessage();
-        $transportMessage->setBody($message->getBody());
-        $transportMessage->setHeaders($headers);
-        $transportMessage->setProperties($properties);
-        $transportMessage->setMessageId($message->getMessageId());
-        $transportMessage->setTimestamp($message->getTimestamp());
-        $transportMessage->setReplyTo($message->getReplyTo());
-        $transportMessage->setCorrelationId($message->getCorrelationId());
-        $transportMessage->setContentType($message->getContentType());
-        $transportMessage->setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT);
-
-        if ($message->getExpire()) {
-            $transportMessage->setExpiration((string) ($message->getExpire() * 1000));
-        }
-
-        return $transportMessage;
-    }
-
-    /**
-     * @param AmqpMessage $message
-     *
-     * {@inheritdoc}
-     */
-    public function createClientMessage(PsrMessage $message)
-    {
-        $clientMessage = new Message();
-
-        $clientMessage->setBody($message->getBody());
-        $clientMessage->setHeaders($message->getHeaders());
-        $clientMessage->setProperties($message->getProperties());
-        $clientMessage->setContentType($message->getContentType());
-
-        if ($expiration = $message->getExpiration()) {
-            if (false == is_numeric($expiration)) {
-                throw new \LogicException(sprintf('expiration header is not numeric. "%s"', $expiration));
-            }
-
-            $clientMessage->setExpire((int) ((int) $expiration) / 1000);
-        }
-
-        $clientMessage->setMessageId($message->getMessageId());
-        $clientMessage->setTimestamp($message->getTimestamp());
-        $clientMessage->setReplyTo($message->getReplyTo());
-        $clientMessage->setCorrelationId($message->getCorrelationId());
-
-        return $clientMessage;
-    }
-
-    /**
-     * @return Config
-     */
-    public function getConfig()
-    {
-        return $this->config;
-    }
-
-    /**
-     * @return AmqpTopic
-     */
-    private function createRouterTopic()
-    {
-        $topic = $this->context->createTopic(
-            $this->config->createTransportRouterTopicName($this->config->getRouterTopicName())
-        );
-        $topic->setType(AmqpTopic::TYPE_FANOUT);
-        $topic->addFlag(AmqpTopic::FLAG_DURABLE);
-
-        return $topic;
-    }
-}
diff --git a/Client/Amqp/RabbitMqDriver.php b/Client/Amqp/RabbitMqDriver.php
deleted file mode 100644
index 518b900..0000000
--- a/Client/Amqp/RabbitMqDriver.php
+++ /dev/null
@@ -1,154 +0,0 @@
-<?php
-
-namespace  Enqueue\Client\Amqp;
-
-use Enqueue\Client\Config;
-use Enqueue\Client\Message;
-use Enqueue\Client\MessagePriority;
-use Enqueue\Client\Meta\QueueMetaRegistry;
-use Enqueue\Consumption\Exception\LogicException;
-use Interop\Amqp\AmqpContext;
-use Interop\Amqp\AmqpMessage;
-use Interop\Amqp\AmqpQueue;
-use Interop\Queue\PsrMessage;
-
-class RabbitMqDriver extends AmqpDriver
-{
-    /**
-     * @var AmqpContext
-     */
-    private $context;
-
-    /**
-     * @var Config
-     */
-    private $config;
-
-    /**
-     * @var QueueMetaRegistry
-     */
-    private $queueMetaRegistry;
-
-    /**
-     * @var array
-     */
-    private $priorityMap;
-
-    /**
-     * @param AmqpContext       $context
-     * @param Config            $config
-     * @param QueueMetaRegistry $queueMetaRegistry
-     */
-    public function __construct(AmqpContext $context, Config $config, QueueMetaRegistry $queueMetaRegistry)
-    {
-        parent::__construct($context, $config, $queueMetaRegistry);
-
-        $this->config = $config;
-        $this->context = $context;
-        $this->queueMetaRegistry = $queueMetaRegistry;
-
-        $this->priorityMap = [
-            MessagePriority::VERY_LOW => 0,
-            MessagePriority::LOW => 1,
-            MessagePriority::NORMAL => 2,
-            MessagePriority::HIGH => 3,
-            MessagePriority::VERY_HIGH => 4,
-        ];
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function sendToProcessor(Message $message)
-    {
-        if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) {
-            throw new \LogicException('Processor name parameter is required but is not set');
-        }
-
-        if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) {
-            throw new \LogicException('Queue name parameter is required but is not set');
-        }
-
-        $transportMessage = $this->createTransportMessage($message);
-        $destination = $this->createQueue($queueName);
-        $producer = $this->context->createProducer();
-
-        if ($message->getDelay()) {
-            $producer->setDeliveryDelay($message->getDelay() * 1000);
-        }
-
-        $producer->send($destination, $transportMessage);
-    }
-
-    /**
-     * {@inheritdoc}
-     *
-     * @return AmqpQueue
-     */
-    public function createQueue($queueName)
-    {
-        $queue = parent::createQueue($queueName);
-        $queue->setArguments(['x-max-priority' => 4]);
-
-        return $queue;
-    }
-
-    /**
-     * {@inheritdoc}
-     *
-     * @return AmqpMessage
-     */
-    public function createTransportMessage(Message $message)
-    {
-        $transportMessage = parent::createTransportMessage($message);
-
-        if ($priority = $message->getPriority()) {
-            if (false == array_key_exists($priority, $this->priorityMap)) {
-                throw new \InvalidArgumentException(sprintf(
-                    'Given priority could not be converted to client\'s one. Got: %s',
-                    $priority
-                ));
-            }
-
-            $transportMessage->setPriority($this->priorityMap[$priority]);
-        }
-
-        if ($message->getDelay()) {
-            if (false == $this->config->getTransportOption('delay_strategy', false)) {
-                throw new LogicException('The message delaying is not supported. In order to use delay feature install RabbitMQ delay strategy.');
-            }
-
-            $transportMessage->setProperty('enqueue-delay', $message->getDelay() * 1000);
-        }
-
-        return $transportMessage;
-    }
-
-    /**
-     * @param AmqpMessage $message
-     *
-     * {@inheritdoc}
-     */
-    public function createClientMessage(PsrMessage $message)
-    {
-        $clientMessage = parent::createClientMessage($message);
-
-        if ($priority = $message->getPriority()) {
-            if (false === $clientPriority = array_search($priority, $this->priorityMap, true)) {
-                throw new \LogicException(sprintf('Cant convert transport priority to client: "%s"', $priority));
-            }
-
-            $clientMessage->setPriority($clientPriority);
-        }
-
-        if ($delay = $message->getProperty('enqueue-delay')) {
-            if (false == is_numeric($delay)) {
-                throw new \LogicException(sprintf('"enqueue-delay" header is not numeric. "%s"', $delay));
-            }
-
-            $clientMessage->setDelay((int) ((int) $delay) / 1000);
-        }
-
-        return $clientMessage;
-    }
-}
diff --git a/Client/ChainExtension.php b/Client/ChainExtension.php
index c202e98..655b75f 100644
--- a/Client/ChainExtension.php
+++ b/Client/ChainExtension.php
@@ -2,38 +2,101 @@
 
 namespace Enqueue\Client;
 
-class ChainExtension implements ExtensionInterface
+final class ChainExtension implements ExtensionInterface
 {
     /**
-     * @var ExtensionInterface[]
+     * @var PreSendEventExtensionInterface[]
      */
-    private $extensions;
+    private $preSendEventExtensions;
 
     /**
-     * @param ExtensionInterface[] $extensions
+     * @var PreSendCommandExtensionInterface[]
      */
+    private $preSendCommandExtensions;
+
+    /**
+     * @var DriverPreSendExtensionInterface[]
+     */
+    private $driverPreSendExtensions;
+
+    /**
+     * @var PostSendExtensionInterface[]
+     */
+    private $postSendExtensions;
+
     public function __construct(array $extensions)
     {
-        $this->extensions = $extensions;
+        $this->preSendEventExtensions = [];
+        $this->preSendCommandExtensions = [];
+        $this->driverPreSendExtensions = [];
+        $this->postSendExtensions = [];
+
+        array_walk($extensions, function ($extension) {
+            if ($extension instanceof ExtensionInterface) {
+                $this->preSendEventExtensions[] = $extension;
+                $this->preSendCommandExtensions[] = $extension;
+                $this->driverPreSendExtensions[] = $extension;
+                $this->postSendExtensions[] = $extension;
+
+                return;
+            }
+
+            $extensionValid = false;
+            if ($extension instanceof PreSendEventExtensionInterface) {
+                $this->preSendEventExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if ($extension instanceof PreSendCommandExtensionInterface) {
+                $this->preSendCommandExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if ($extension instanceof DriverPreSendExtensionInterface) {
+                $this->driverPreSendExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if ($extension instanceof PostSendExtensionInterface) {
+                $this->postSendExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if (false == $extensionValid) {
+                throw new \LogicException(sprintf('Invalid extension given %s', $extension::class));
+            }
+        });
+    }
+
+    public function onPreSendEvent(PreSend $context): void
+    {
+        foreach ($this->preSendEventExtensions as $extension) {
+            $extension->onPreSendEvent($context);
+        }
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onPreSend($topic, Message $message)
+    public function onPreSendCommand(PreSend $context): void
     {
-        foreach ($this->extensions as $extension) {
-            $extension->onPreSend($topic, $message);
+        foreach ($this->preSendCommandExtensions as $extension) {
+            $extension->onPreSendCommand($context);
         }
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onPostSend($topic, Message $message)
+    public function onDriverPreSend(DriverPreSend $context): void
+    {
+        foreach ($this->driverPreSendExtensions as $extension) {
+            $extension->onDriverPreSend($context);
+        }
+    }
+
+    public function onPostSend(PostSend $context): void
     {
-        foreach ($this->extensions as $extension) {
-            $extension->onPostSend($topic, $message);
+        foreach ($this->postSendExtensions as $extension) {
+            $extension->onPostSend($context);
         }
     }
 }
diff --git a/Client/CommandSubscriberInterface.php b/Client/CommandSubscriberInterface.php
index 99f6b27..d7b06da 100644
--- a/Client/CommandSubscriberInterface.php
+++ b/Client/CommandSubscriberInterface.php
@@ -2,6 +2,15 @@
 
 namespace Enqueue\Client;
 
+/**
+ * @phpstan-type CommandConfig = array{
+ *     command: string,
+ *     processor?: string,
+ *     queue?: string,
+ *     prefix_queue?: bool,
+ *     exclusive?: bool,
+ * }
+ */
 interface CommandSubscriberInterface
 {
     /**
@@ -12,17 +21,40 @@ interface CommandSubscriberInterface
      * or
      *
      * [
-     *   'processorName' => 'aCommandName',
-     *   'queueName' => 'a_client_queue_name',
-     *   'queueNameHardcoded' => true,
+     *   'command' => 'aSubscribedCommand',
+     *   'processor' => 'aProcessorName',
+     *   'queue' => 'a_client_queue_name',
+     *   'prefix_queue' => true,
      *   'exclusive' => true,
      * ]
      *
-     * queueName, exclusive and queueNameHardcoded are optional.
+     * or
+     *
+     * [
+     *   [
+     *     'command' => 'aSubscribedCommand',
+     *     'processor' => 'aProcessorName',
+     *     'queue' => 'a_client_queue_name',
+     *     'prefix_queue' => true,
+     *     'exclusive' => true,
+     *   ],
+     *   [
+     *     'command' => 'aSubscribedCommand',
+     *     'processor' => 'aProcessorName',
+     *     'queue' => 'a_client_queue_name',
+     *     'prefix_queue' => true,
+     *     'exclusive' => true,
+     *   ]
+     * ]
+     *
+     * queue, processor, prefix_queue, and exclusive are optional.
+     * It is possible to pass other options, they could be accessible on a route instance through options.
      *
-     * Note: If you set queueNameHardcoded to true then the queueName is used as is and therefor the driver is not used to create a transport queue name.
+     * Note: If you set "prefix_queue" to true then the "queue" is used as is and therefor the driver is not used to create a transport queue name.
      *
      * @return string|array
+     *
+     * @phpstan-return string|CommandConfig|array<CommandConfig>
      */
     public static function getSubscribedCommand();
 }
diff --git a/Client/Config.php b/Client/Config.php
index d5d8b07..8210dff 100644
--- a/Client/Config.php
+++ b/Client/Config.php
@@ -4,12 +4,13 @@
 
 class Config
 {
-    const PARAMETER_TOPIC_NAME = 'enqueue.topic_name';
-    const PARAMETER_COMMAND_NAME = 'enqueue.command_name';
-    const PARAMETER_PROCESSOR_NAME = 'enqueue.processor_name';
-    const PARAMETER_PROCESSOR_QUEUE_NAME = 'enqueue.processor_queue_name';
-    const DEFAULT_PROCESSOR_QUEUE_NAME = 'default';
-    const COMMAND_TOPIC = '__command__';
+    public const TOPIC = 'enqueue.topic';
+    public const COMMAND = 'enqueue.command';
+    public const PROCESSOR = 'enqueue.processor';
+    public const EXPIRE = 'enqueue.expire';
+    public const PRIORITY = 'enqueue.priority';
+    public const DELAY = 'enqueue.delay';
+    public const CONTENT_TYPE = 'enqueue.content_type';
 
     /**
      * @var string
@@ -19,27 +20,32 @@ class Config
     /**
      * @var string
      */
-    private $appName;
+    private $separator;
 
     /**
      * @var string
      */
-    private $routerTopicName;
+    private $app;
 
     /**
      * @var string
      */
-    private $routerQueueName;
+    private $routerTopic;
 
     /**
      * @var string
      */
-    private $defaultProcessorQueueName;
+    private $routerQueue;
 
     /**
      * @var string
      */
-    private $routerProcessorName;
+    private $defaultQueue;
+
+    /**
+     * @var string
+     */
+    private $routerProcessor;
 
     /**
      * @var array
@@ -47,116 +53,126 @@ class Config
     private $transportConfig;
 
     /**
-     * @param string $prefix
-     * @param string $appName
-     * @param string $routerTopicName
-     * @param string $routerQueueName
-     * @param string $defaultProcessorQueueName
-     * @param string $routerProcessorName
-     * @param array  $transportConfig
+     * @var array
      */
-    public function __construct($prefix, $appName, $routerTopicName, $routerQueueName, $defaultProcessorQueueName, $routerProcessorName, array $transportConfig = [])
-    {
-        $this->prefix = $prefix;
-        $this->appName = $appName;
-        $this->routerTopicName = $routerTopicName;
-        $this->routerQueueName = $routerQueueName;
-        $this->defaultProcessorQueueName = $defaultProcessorQueueName;
-        $this->routerProcessorName = $routerProcessorName;
+    private $driverConfig;
+
+    public function __construct(
+        string $prefix,
+        string $separator,
+        string $app,
+        string $routerTopic,
+        string $routerQueue,
+        string $defaultQueue,
+        string $routerProcessor,
+        array $transportConfig,
+        array $driverConfig,
+    ) {
+        $this->prefix = trim($prefix);
+        $this->app = trim($app);
+
+        $this->routerTopic = trim($routerTopic);
+        if (empty($this->routerTopic)) {
+            throw new \InvalidArgumentException('Router topic is empty.');
+        }
+
+        $this->routerQueue = trim($routerQueue);
+        if (empty($this->routerQueue)) {
+            throw new \InvalidArgumentException('Router queue is empty.');
+        }
+
+        $this->defaultQueue = trim($defaultQueue);
+        if (empty($this->defaultQueue)) {
+            throw new \InvalidArgumentException('Default processor queue name is empty.');
+        }
+
+        $this->routerProcessor = trim($routerProcessor);
+        if (empty($this->routerProcessor)) {
+            throw new \InvalidArgumentException('Router processor name is empty.');
+        }
+
         $this->transportConfig = $transportConfig;
+        $this->driverConfig = $driverConfig;
+
+        $this->separator = $separator;
     }
 
-    /**
-     * @return string
-     */
-    public function getRouterTopicName()
+    public function getPrefix(): string
     {
-        return $this->routerTopicName;
+        return $this->prefix;
     }
 
-    /**
-     * @return string
-     */
-    public function getRouterQueueName()
+    public function getSeparator(): string
     {
-        return $this->routerQueueName;
+        return $this->separator;
     }
 
-    /**
-     * @return string
-     */
-    public function getDefaultProcessorQueueName()
+    public function getApp(): string
     {
-        return $this->defaultProcessorQueueName;
+        return $this->app;
     }
 
-    /**
-     * @return string
-     */
-    public function getRouterProcessorName()
+    public function getRouterTopic(): string
     {
-        return $this->routerProcessorName;
+        return $this->routerTopic;
     }
 
-    /**
-     * @param string $name
-     *
-     * @return string
-     */
-    public function createTransportRouterTopicName($name)
+    public function getRouterQueue(): string
     {
-        return strtolower(implode('.', array_filter([trim($this->prefix), trim($name)])));
+        return $this->routerQueue;
     }
 
-    /**
-     * @param string $name
-     *
-     * @return string
-     */
-    public function createTransportQueueName($name)
+    public function getDefaultQueue(): string
     {
-        return strtolower(implode('.', array_filter([trim($this->prefix), trim($this->appName), trim($name)])));
+        return $this->defaultQueue;
     }
 
-    /**
-     * @param string     $name
-     * @param mixed|null $default
-     *
-     * @return array
-     */
-    public function getTransportOption($name, $default = null)
+    public function getRouterProcessor(): string
+    {
+        return $this->routerProcessor;
+    }
+
+    public function getTransportOption(string $name, $default = null)
     {
         return array_key_exists($name, $this->transportConfig) ? $this->transportConfig[$name] : $default;
     }
 
-    /**
-     * @param string|null $prefix
-     * @param string|null $appName
-     * @param string|null $routerTopicName
-     * @param string|null $routerQueueName
-     * @param string|null $defaultProcessorQueueName
-     * @param string|null $routerProcessorName
-     * @param array       $transportConfig
-     *
-     * @return static
-     */
+    public function getTransportOptions(): array
+    {
+        return $this->transportConfig;
+    }
+
+    public function getDriverOption(string $name, $default = null)
+    {
+        return array_key_exists($name, $this->driverConfig) ? $this->driverConfig[$name] : $default;
+    }
+
+    public function getDriverOptions(): array
+    {
+        return $this->driverConfig;
+    }
+
     public static function create(
-        $prefix = null,
-        $appName = null,
-        $routerTopicName = null,
-        $routerQueueName = null,
-        $defaultProcessorQueueName = null,
-        $routerProcessorName = null,
-        array $transportConfig = []
-    ) {
-        return new static(
+        ?string $prefix = null,
+        ?string $separator = null,
+        ?string $app = null,
+        ?string $routerTopic = null,
+        ?string $routerQueue = null,
+        ?string $defaultQueue = null,
+        ?string $routerProcessor = null,
+        array $transportConfig = [],
+        array $driverConfig = [],
+    ): self {
+        return new self(
             $prefix ?: '',
-            $appName ?: '',
-            $routerTopicName ?: 'router',
-            $routerQueueName ?: 'default',
-            $defaultProcessorQueueName ?: 'default',
-            $routerProcessorName ?: 'router',
-            $transportConfig
+            $separator ?: '.',
+            $app ?: '',
+            $routerTopic ?: 'router',
+            $routerQueue ?: 'default',
+            $defaultQueue ?: 'default',
+            $routerProcessor ?: 'router',
+            $transportConfig,
+            $driverConfig
         );
     }
 }
diff --git a/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php b/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php
index 98fa483..475e2cf 100644
--- a/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php
+++ b/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php
@@ -3,16 +3,13 @@
 namespace Enqueue\Client\ConsumptionExtension;
 
 use Enqueue\Client\DriverInterface;
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\EmptyExtensionTrait;
-use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\MessageReceivedExtensionInterface;
 use Enqueue\Consumption\Result;
 
-class DelayRedeliveredMessageExtension implements ExtensionInterface
+class DelayRedeliveredMessageExtension implements MessageReceivedExtensionInterface
 {
-    use EmptyExtensionTrait;
-
-    const PROPERTY_REDELIVER_COUNT = 'enqueue.redelivery_count';
+    public const PROPERTY_REDELIVER_COUNT = 'enqueue.redelivery_count';
 
     /**
      * @var DriverInterface
@@ -27,8 +24,7 @@ class DelayRedeliveredMessageExtension implements ExtensionInterface
     private $delay;
 
     /**
-     * @param DriverInterface $driver
-     * @param int             $delay  The number of seconds the message should be delayed
+     * @param int $delay The number of seconds the message should be delayed
      */
     public function __construct(DriverInterface $driver, $delay)
     {
@@ -36,12 +32,9 @@ public function __construct(DriverInterface $driver, $delay)
         $this->delay = $delay;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onPreReceived(Context $context)
+    public function onMessageReceived(MessageReceived $context): void
     {
-        $message = $context->getPsrMessage();
+        $message = $context->getMessage();
         if (false == $message->isRedelivered()) {
             return;
         }
diff --git a/Client/ConsumptionExtension/ExclusiveCommandExtension.php b/Client/ConsumptionExtension/ExclusiveCommandExtension.php
index a9afde0..7ab88ae 100644
--- a/Client/ConsumptionExtension/ExclusiveCommandExtension.php
+++ b/Client/ConsumptionExtension/ExclusiveCommandExtension.php
@@ -3,83 +3,75 @@
 namespace Enqueue\Client\ConsumptionExtension;
 
 use Enqueue\Client\Config;
-use Enqueue\Client\ExtensionInterface as ClientExtensionInterface;
-use Enqueue\Client\Message;
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\EmptyExtensionTrait;
-use Enqueue\Consumption\ExtensionInterface as ConsumptionExtensionInterface;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Route;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\MessageReceivedExtensionInterface;
 
-class ExclusiveCommandExtension implements ConsumptionExtensionInterface, ClientExtensionInterface
+final class ExclusiveCommandExtension implements MessageReceivedExtensionInterface
 {
-    use EmptyExtensionTrait;
-
     /**
-     * @var string[]
+     * @var DriverInterface
      */
-    private $queueNameToProcessorNameMap;
+    private $driver;
 
     /**
-     * @var string[]
+     * @var Route[]
      */
-    private $processorNameToQueueNameMap;
+    private $queueToRouteMap;
 
-    /**
-     * @param string[] $queueNameToProcessorNameMap
-     */
-    public function __construct(array $queueNameToProcessorNameMap)
+    public function __construct(DriverInterface $driver)
     {
-        $this->queueNameToProcessorNameMap = $queueNameToProcessorNameMap;
-        $this->processorNameToQueueNameMap = array_flip($queueNameToProcessorNameMap);
+        $this->driver = $driver;
     }
 
-    public function onPreReceived(Context $context)
+    public function onMessageReceived(MessageReceived $context): void
     {
-        $message = $context->getPsrMessage();
-        $queue = $context->getPsrQueue();
-
-        if ($message->getProperty(Config::PARAMETER_TOPIC_NAME)) {
+        $message = $context->getMessage();
+        if ($message->getProperty(Config::TOPIC)) {
             return;
         }
-        if ($message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) {
+        if ($message->getProperty(Config::COMMAND)) {
             return;
         }
-        if ($message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) {
+        if ($message->getProperty(Config::PROCESSOR)) {
             return;
         }
-        if ($message->getProperty(Config::PARAMETER_COMMAND_NAME)) {
-            return;
+
+        if (null === $this->queueToRouteMap) {
+            $this->queueToRouteMap = $this->buildMap();
         }
 
-        if (array_key_exists($queue->getQueueName(), $this->queueNameToProcessorNameMap)) {
+        $queue = $context->getConsumer()->getQueue();
+        if (array_key_exists($queue->getQueueName(), $this->queueToRouteMap)) {
             $context->getLogger()->debug('[ExclusiveCommandExtension] This is a exclusive command queue and client\'s properties are not set. Setting them');
 
-            $message->setProperty(Config::PARAMETER_TOPIC_NAME, Config::COMMAND_TOPIC);
-            $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $queue->getQueueName());
-            $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, $this->queueNameToProcessorNameMap[$queue->getQueueName()]);
-            $message->setProperty(Config::PARAMETER_COMMAND_NAME, $this->queueNameToProcessorNameMap[$queue->getQueueName()]);
+            $route = $this->queueToRouteMap[$queue->getQueueName()];
+            $message->setProperty(Config::PROCESSOR, $route->getProcessor());
+            $message->setProperty(Config::COMMAND, $route->getSource());
         }
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onPreSend($topic, Message $message)
+    private function buildMap(): array
     {
-        if (Config::COMMAND_TOPIC != $topic) {
-            return;
-        }
+        $map = [];
+        foreach ($this->driver->getRouteCollection()->all() as $route) {
+            if (false == $route->isCommand()) {
+                continue;
+            }
+
+            if (false == $route->isProcessorExclusive()) {
+                continue;
+            }
+
+            $queueName = $this->driver->createRouteQueue($route)->getQueueName();
+            if (array_key_exists($queueName, $map)) {
+                throw new \LogicException('The queue name has been already bound by another exclusive command processor');
+            }
 
-        $commandName = $message->getProperty(Config::PARAMETER_COMMAND_NAME);
-        if (array_key_exists($commandName, $this->processorNameToQueueNameMap)) {
-            $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, $commandName);
-            $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $this->processorNameToQueueNameMap[$commandName]);
+            $map[$queueName] = $route;
         }
-    }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onPostSend($topic, Message $message)
-    {
+        return $map;
     }
 }
diff --git a/Client/ConsumptionExtension/FlushSpoolProducerExtension.php b/Client/ConsumptionExtension/FlushSpoolProducerExtension.php
index efb6848..6682cad 100644
--- a/Client/ConsumptionExtension/FlushSpoolProducerExtension.php
+++ b/Client/ConsumptionExtension/FlushSpoolProducerExtension.php
@@ -3,36 +3,29 @@
 namespace Enqueue\Client\ConsumptionExtension;
 
 use Enqueue\Client\SpoolProducer;
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\EmptyExtensionTrait;
-use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\Context\End;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\EndExtensionInterface;
+use Enqueue\Consumption\PostMessageReceivedExtensionInterface;
 
-class FlushSpoolProducerExtension implements ExtensionInterface
+class FlushSpoolProducerExtension implements PostMessageReceivedExtensionInterface, EndExtensionInterface
 {
-    use EmptyExtensionTrait;
-
     /**
      * @var SpoolProducer
      */
     private $producer;
 
-    /**
-     * @param SpoolProducer $producer
-     */
     public function __construct(SpoolProducer $producer)
     {
         $this->producer = $producer;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onPostReceived(Context $context)
+    public function onPostMessageReceived(PostMessageReceived $context): void
     {
         $this->producer->flush();
     }
 
-    public function onInterrupted(Context $context)
+    public function onEnd(End $context): void
     {
         $this->producer->flush();
     }
diff --git a/Client/ConsumptionExtension/LogExtension.php b/Client/ConsumptionExtension/LogExtension.php
new file mode 100644
index 0000000..693be20
--- /dev/null
+++ b/Client/ConsumptionExtension/LogExtension.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Enqueue\Client\ConsumptionExtension;
+
+use Enqueue\Client\Config;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Result;
+use Enqueue\Util\Stringify;
+use Psr\Log\LogLevel;
+
+class LogExtension extends \Enqueue\Consumption\Extension\LogExtension
+{
+    public function onPostMessageReceived(PostMessageReceived $context): void
+    {
+        $result = $context->getResult();
+        $message = $context->getMessage();
+
+        $logLevel = Result::REJECT == ((string) $result) ? LogLevel::ERROR : LogLevel::INFO;
+
+        if ($command = $message->getProperty(Config::COMMAND)) {
+            $reason = '';
+            $logMessage = "[client] Processed {command}\t{body}\t{result}";
+            if ($result instanceof Result && $result->getReason()) {
+                $reason = $result->getReason();
+
+                $logMessage .= ' {reason}';
+            }
+
+            $context->getLogger()->log($logLevel, $logMessage, [
+                'result' => str_replace('enqueue.', '', $result),
+                'reason' => $reason,
+                'command' => $command,
+                'queueName' => $context->getConsumer()->getQueue()->getQueueName(),
+                'body' => Stringify::that($message->getBody()),
+                'properties' => Stringify::that($message->getProperties()),
+                'headers' => Stringify::that($message->getHeaders()),
+            ]);
+
+            return;
+        }
+
+        $topic = $message->getProperty(Config::TOPIC);
+        $processor = $message->getProperty(Config::PROCESSOR);
+        if ($topic && $processor) {
+            $reason = '';
+            $logMessage = "[client] Processed {topic} -> {processor}\t{body}\t{result}";
+            if ($result instanceof Result && $result->getReason()) {
+                $reason = $result->getReason();
+
+                $logMessage .= ' {reason}';
+            }
+
+            $context->getLogger()->log($logLevel, $logMessage, [
+                'result' => str_replace('enqueue.', '', $result),
+                'reason' => $reason,
+                'topic' => $topic,
+                'processor' => $processor,
+                'queueName' => $context->getConsumer()->getQueue()->getQueueName(),
+                'body' => Stringify::that($message->getBody()),
+                'properties' => Stringify::that($message->getProperties()),
+                'headers' => Stringify::that($message->getHeaders()),
+            ]);
+
+            return;
+        }
+
+        parent::onPostMessageReceived($context);
+    }
+}
diff --git a/Client/ConsumptionExtension/SetRouterPropertiesExtension.php b/Client/ConsumptionExtension/SetRouterPropertiesExtension.php
index 44b7ea4..0d22783 100644
--- a/Client/ConsumptionExtension/SetRouterPropertiesExtension.php
+++ b/Client/ConsumptionExtension/SetRouterPropertiesExtension.php
@@ -4,44 +4,43 @@
 
 use Enqueue\Client\Config;
 use Enqueue\Client\DriverInterface;
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\EmptyExtensionTrait;
-use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\MessageReceivedExtensionInterface;
 
-class SetRouterPropertiesExtension implements ExtensionInterface
+class SetRouterPropertiesExtension implements MessageReceivedExtensionInterface
 {
-    use EmptyExtensionTrait;
-
     /**
      * @var DriverInterface
      */
     private $driver;
 
-    /**
-     * @param DriverInterface $driver
-     */
     public function __construct(DriverInterface $driver)
     {
         $this->driver = $driver;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onPreReceived(Context $context)
+    public function onMessageReceived(MessageReceived $context): void
     {
-        $message = $context->getPsrMessage();
-        if ($message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) {
+        $message = $context->getMessage();
+        if (false == $message->getProperty(Config::TOPIC)) {
+            return;
+        }
+        if ($message->getProperty(Config::PROCESSOR)) {
             return;
         }
 
         $config = $this->driver->getConfig();
-        if ($context->getPsrQueue()->getQueueName() != $config->createTransportQueueName($config->getRouterQueueName())) {
+        $queue = $this->driver->createQueue($config->getRouterQueue());
+        if ($context->getConsumer()->getQueue()->getQueueName() != $queue->getQueueName()) {
             return;
         }
 
         // RouterProcessor is our default message processor when that header is not set
-        $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, $config->getRouterProcessorName());
-        $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $config->getRouterQueueName());
+        $message->setProperty(Config::PROCESSOR, $config->getRouterProcessor());
+
+        $context->getLogger()->debug(
+            '[SetRouterPropertiesExtension] '.
+            sprintf('Set router processor "%s"', $config->getRouterProcessor())
+        );
     }
 }
diff --git a/Client/ConsumptionExtension/SetupBrokerExtension.php b/Client/ConsumptionExtension/SetupBrokerExtension.php
index 8b6aecb..44d610f 100644
--- a/Client/ConsumptionExtension/SetupBrokerExtension.php
+++ b/Client/ConsumptionExtension/SetupBrokerExtension.php
@@ -3,14 +3,11 @@
 namespace Enqueue\Client\ConsumptionExtension;
 
 use Enqueue\Client\DriverInterface;
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\EmptyExtensionTrait;
-use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\Context\Start;
+use Enqueue\Consumption\StartExtensionInterface;
 
-class SetupBrokerExtension implements ExtensionInterface
+class SetupBrokerExtension implements StartExtensionInterface
 {
-    use EmptyExtensionTrait;
-
     /**
      * @var DriverInterface
      */
@@ -21,19 +18,13 @@ class SetupBrokerExtension implements ExtensionInterface
      */
     private $isDone;
 
-    /**
-     * @param DriverInterface $driver
-     */
     public function __construct(DriverInterface $driver)
     {
         $this->driver = $driver;
         $this->isDone = false;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onStart(Context $context)
+    public function onStart(Start $context): void
     {
         if (false == $this->isDone) {
             $this->isDone = true;
diff --git a/Client/DelegateProcessor.php b/Client/DelegateProcessor.php
index 6cb8f98..7582c52 100644
--- a/Client/DelegateProcessor.php
+++ b/Client/DelegateProcessor.php
@@ -2,36 +2,31 @@
 
 namespace Enqueue\Client;
 
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrMessage;
-use Interop\Queue\PsrProcessor;
+use Enqueue\ProcessorRegistryInterface;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Processor;
 
-class DelegateProcessor implements PsrProcessor
+class DelegateProcessor implements Processor
 {
     /**
      * @var ProcessorRegistryInterface
      */
     private $registry;
 
-    /**
-     * @param ProcessorRegistryInterface $registry
-     */
     public function __construct(ProcessorRegistryInterface $registry)
     {
         $this->registry = $registry;
     }
 
     /**
-     * {@inheritdoc}
+     * @return string|object
      */
-    public function process(PsrMessage $message, PsrContext $context)
+    public function process(InteropMessage $message, Context $context)
     {
-        $processorName = $message->getProperty(Config::PARAMETER_PROCESSOR_NAME);
+        $processorName = $message->getProperty(Config::PROCESSOR);
         if (false == $processorName) {
-            throw new \LogicException(sprintf(
-                'Got message without required parameter: "%s"',
-                Config::PARAMETER_PROCESSOR_NAME
-            ));
+            throw new \LogicException(sprintf('Got message without required parameter: "%s"', Config::PROCESSOR));
         }
 
         return $this->registry->get($processorName)->process($message, $context);
diff --git a/Client/Driver/AmqpDriver.php b/Client/Driver/AmqpDriver.php
new file mode 100644
index 0000000..1def3fb
--- /dev/null
+++ b/Client/Driver/AmqpDriver.php
@@ -0,0 +1,132 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Enqueue\Client\Driver;
+
+use Enqueue\AmqpExt\AmqpProducer;
+use Enqueue\Client\Message;
+use Interop\Amqp\AmqpContext;
+use Interop\Amqp\AmqpMessage;
+use Interop\Amqp\AmqpQueue;
+use Interop\Amqp\AmqpTopic;
+use Interop\Amqp\Impl\AmqpBind;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * @method AmqpContext getContext
+ */
+class AmqpDriver extends GenericDriver
+{
+    public function __construct(AmqpContext $context, ...$args)
+    {
+        parent::__construct($context, ...$args);
+    }
+
+    /**
+     * @return AmqpMessage
+     */
+    public function createTransportMessage(Message $clientMessage): InteropMessage
+    {
+        /** @var AmqpMessage $transportMessage */
+        $transportMessage = parent::createTransportMessage($clientMessage);
+        $transportMessage->setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT);
+        $transportMessage->setContentType($clientMessage->getContentType());
+
+        if ($clientMessage->getExpire()) {
+            $transportMessage->setExpiration($clientMessage->getExpire() * 1000);
+        }
+
+        $priorityMap = $this->getPriorityMap();
+        if ($priority = $clientMessage->getPriority()) {
+            if (false == array_key_exists($priority, $priorityMap)) {
+                throw new \InvalidArgumentException(sprintf('Cant convert client priority "%s" to transport one. Could be one of "%s"', $priority, implode('", "', array_keys($priorityMap))));
+            }
+
+            $transportMessage->setPriority($priorityMap[$priority]);
+        }
+
+        return $transportMessage;
+    }
+
+    public function setupBroker(?LoggerInterface $logger = null): void
+    {
+        $logger = $logger ?: new NullLogger();
+        $log = function ($text, ...$args) use ($logger) {
+            $logger->debug(sprintf('[AmqpDriver] '.$text, ...$args));
+        };
+
+        // setup router
+        $routerTopic = $this->createRouterTopic();
+        $log('Declare router exchange: %s', $routerTopic->getTopicName());
+        $this->getContext()->declareTopic($routerTopic);
+
+        $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue());
+        $log('Declare router queue: %s', $routerQueue->getQueueName());
+        $this->getContext()->declareQueue($routerQueue);
+
+        $log('Bind router queue to exchange: %s -> %s', $routerQueue->getQueueName(), $routerTopic->getTopicName());
+        $this->getContext()->bind(new AmqpBind($routerTopic, $routerQueue, $routerQueue->getQueueName()));
+
+        // setup queues
+        $declaredQueues = [];
+        foreach ($this->getRouteCollection()->all() as $route) {
+            /** @var AmqpQueue $queue */
+            $queue = $this->createRouteQueue($route);
+            if (array_key_exists($queue->getQueueName(), $declaredQueues)) {
+                continue;
+            }
+
+            $log('Declare processor queue: %s', $queue->getQueueName());
+            $this->getContext()->declareQueue($queue);
+
+            $declaredQueues[$queue->getQueueName()] = true;
+        }
+    }
+
+    /**
+     * @return AmqpTopic
+     */
+    protected function createRouterTopic(): Destination
+    {
+        $topic = $this->doCreateTopic(
+            $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true)
+        );
+        $topic->setType(AmqpTopic::TYPE_FANOUT);
+        $topic->addFlag(AmqpTopic::FLAG_DURABLE);
+
+        return $topic;
+    }
+
+    /**
+     * @return AmqpQueue
+     */
+    protected function doCreateQueue(string $transportQueueName): InteropQueue
+    {
+        /** @var AmqpQueue $queue */
+        $queue = parent::doCreateQueue($transportQueueName);
+        $queue->addFlag(AmqpQueue::FLAG_DURABLE);
+
+        return $queue;
+    }
+
+    /**
+     * @param AmqpProducer $producer
+     * @param AmqpTopic    $topic
+     * @param AmqpMessage  $transportMessage
+     */
+    protected function doSendToRouter(InteropProducer $producer, Destination $topic, InteropMessage $transportMessage): void
+    {
+        // We should not handle priority, expiration, and delay at this stage.
+        // The router will take care of it while re-sending the message to the final destinations.
+        $transportMessage->setPriority(null);
+        $transportMessage->setExpiration(null);
+
+        $producer->send($topic, $transportMessage);
+    }
+}
diff --git a/Client/Driver/DbalDriver.php b/Client/Driver/DbalDriver.php
new file mode 100644
index 0000000..34875ef
--- /dev/null
+++ b/Client/Driver/DbalDriver.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Enqueue\Client\Driver;
+
+use Enqueue\Dbal\DbalContext;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * @method DbalContext getContext
+ */
+class DbalDriver extends GenericDriver
+{
+    public function __construct(DbalContext $context, ...$args)
+    {
+        parent::__construct($context, ...$args);
+    }
+
+    public function setupBroker(?LoggerInterface $logger = null): void
+    {
+        $logger = $logger ?: new NullLogger();
+        $log = function ($text, ...$args) use ($logger) {
+            $logger->debug(sprintf('[DbalDriver] '.$text, ...$args));
+        };
+
+        $log('Creating database table: "%s"', $this->getContext()->getTableName());
+        $this->getContext()->createDataBaseTable();
+    }
+}
diff --git a/Client/Driver/FsDriver.php b/Client/Driver/FsDriver.php
new file mode 100644
index 0000000..f578b17
--- /dev/null
+++ b/Client/Driver/FsDriver.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Enqueue\Client\Driver;
+
+use Enqueue\Fs\FsContext;
+use Enqueue\Fs\FsDestination;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * @method FsContext getContext
+ * @method FsDestination createQueue(string $name)
+ */
+class FsDriver extends GenericDriver
+{
+    public function __construct(FsContext $context, ...$args)
+    {
+        parent::__construct($context, ...$args);
+    }
+
+    public function setupBroker(?LoggerInterface $logger = null): void
+    {
+        $logger = $logger ?: new NullLogger();
+        $log = function ($text, ...$args) use ($logger) {
+            $logger->debug(sprintf('[FsDriver] '.$text, ...$args));
+        };
+
+        // setup router
+        $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue());
+
+        $log('Declare router queue "%s" file: %s', $routerQueue->getQueueName(), $routerQueue->getFileInfo());
+        $this->getContext()->declareDestination($routerQueue);
+
+        // setup queues
+        $declaredQueues = [];
+        foreach ($this->getRouteCollection()->all() as $route) {
+            /** @var FsDestination $queue */
+            $queue = $this->createRouteQueue($route);
+            if (array_key_exists($queue->getQueueName(), $declaredQueues)) {
+                continue;
+            }
+
+            $log('Declare processor queue "%s" file: %s', $queue->getQueueName(), $queue->getFileInfo());
+            $this->getContext()->declareDestination($queue);
+
+            $declaredQueues[$queue->getQueueName()] = true;
+        }
+    }
+}
diff --git a/Client/Driver/GenericDriver.php b/Client/Driver/GenericDriver.php
new file mode 100644
index 0000000..d509677
--- /dev/null
+++ b/Client/Driver/GenericDriver.php
@@ -0,0 +1,278 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Enqueue\Client\Driver;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\DriverSendResult;
+use Enqueue\Client\Message;
+use Enqueue\Client\MessagePriority;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Interop\Queue\Context;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use Interop\Queue\Topic as InteropTopic;
+use Psr\Log\LoggerInterface;
+
+class GenericDriver implements DriverInterface
+{
+    /**
+     * @var Context
+     */
+    private $context;
+
+    /**
+     * @var Config
+     */
+    private $config;
+
+    /**
+     * @var RouteCollection
+     */
+    private $routeCollection;
+
+    public function __construct(
+        Context $context,
+        Config $config,
+        RouteCollection $routeCollection,
+    ) {
+        $this->context = $context;
+        $this->config = $config;
+        $this->routeCollection = $routeCollection;
+    }
+
+    public function sendToRouter(Message $message): DriverSendResult
+    {
+        if ($message->getProperty(Config::COMMAND)) {
+            throw new \LogicException('Command must not be send to router but go directly to its processor.');
+        }
+        if (false == $message->getProperty(Config::TOPIC)) {
+            throw new \LogicException('Topic name parameter is required but is not set');
+        }
+
+        $topic = $this->createRouterTopic();
+        $transportMessage = $this->createTransportMessage($message);
+        $producer = $this->getContext()->createProducer();
+
+        $this->doSendToRouter($producer, $topic, $transportMessage);
+
+        return new DriverSendResult($topic, $transportMessage);
+    }
+
+    public function sendToProcessor(Message $message): DriverSendResult
+    {
+        $topic = $message->getProperty(Config::TOPIC);
+        $command = $message->getProperty(Config::COMMAND);
+
+        /** @var InteropQueue $queue */
+        $queue = null;
+        $routerProcessor = $this->config->getRouterProcessor();
+        $processor = $message->getProperty(Config::PROCESSOR);
+        if ($topic && $processor && $processor !== $routerProcessor) {
+            $route = $this->routeCollection->topicAndProcessor($topic, $processor);
+            if (false == $route) {
+                throw new \LogicException(sprintf('There is no route for topic "%s" and processor "%s"', $topic, $processor));
+            }
+
+            $message->setProperty(Config::PROCESSOR, $route->getProcessor());
+            $queue = $this->createRouteQueue($route);
+        } elseif ($topic && (false == $processor || $processor === $routerProcessor)) {
+            $message->setProperty(Config::PROCESSOR, $routerProcessor);
+
+            $queue = $this->createQueue($this->config->getRouterQueue());
+        } elseif ($command) {
+            $route = $this->routeCollection->command($command);
+            if (false == $route) {
+                throw new \LogicException(sprintf('There is no route for command "%s".', $command));
+            }
+
+            $message->setProperty(Config::PROCESSOR, $route->getProcessor());
+            $queue = $this->createRouteQueue($route);
+        } else {
+            throw new \LogicException('Either topic or command parameter must be set.');
+        }
+
+        $transportMessage = $this->createTransportMessage($message);
+
+        $producer = $this->context->createProducer();
+
+        if (null !== $delay = $transportMessage->getProperty(Config::DELAY)) {
+            $producer->setDeliveryDelay($delay * 1000);
+        }
+
+        if (null !== $expire = $transportMessage->getProperty(Config::EXPIRE)) {
+            $producer->setTimeToLive($expire * 1000);
+        }
+
+        if (null !== $priority = $transportMessage->getProperty(Config::PRIORITY)) {
+            $priorityMap = $this->getPriorityMap();
+
+            $producer->setPriority($priorityMap[$priority]);
+        }
+
+        $this->doSendToProcessor($producer, $queue, $transportMessage);
+
+        return new DriverSendResult($queue, $transportMessage);
+    }
+
+    public function setupBroker(?LoggerInterface $logger = null): void
+    {
+    }
+
+    public function createQueue(string $clientQueueName, bool $prefix = true): InteropQueue
+    {
+        $transportName = $this->createTransportQueueName($clientQueueName, $prefix);
+
+        return $this->doCreateQueue($transportName);
+    }
+
+    public function createRouteQueue(Route $route): InteropQueue
+    {
+        $transportName = $this->createTransportQueueName(
+            $route->getQueue() ?: $this->config->getDefaultQueue(),
+            $route->isPrefixQueue()
+        );
+
+        return $this->doCreateQueue($transportName);
+    }
+
+    public function createTransportMessage(Message $clientMessage): InteropMessage
+    {
+        $headers = $clientMessage->getHeaders();
+        $properties = $clientMessage->getProperties();
+
+        $transportMessage = $this->context->createMessage();
+        $transportMessage->setBody($clientMessage->getBody());
+        $transportMessage->setHeaders($headers);
+        $transportMessage->setProperties($properties);
+        $transportMessage->setMessageId($clientMessage->getMessageId());
+        $transportMessage->setTimestamp($clientMessage->getTimestamp());
+        $transportMessage->setReplyTo($clientMessage->getReplyTo());
+        $transportMessage->setCorrelationId($clientMessage->getCorrelationId());
+
+        if ($contentType = $clientMessage->getContentType()) {
+            $transportMessage->setProperty(Config::CONTENT_TYPE, $contentType);
+        }
+
+        if ($priority = $clientMessage->getPriority()) {
+            $transportMessage->setProperty(Config::PRIORITY, $priority);
+        }
+
+        if ($expire = $clientMessage->getExpire()) {
+            $transportMessage->setProperty(Config::EXPIRE, $expire);
+        }
+
+        if ($delay = $clientMessage->getDelay()) {
+            $transportMessage->setProperty(Config::DELAY, $delay);
+        }
+
+        return $transportMessage;
+    }
+
+    public function createClientMessage(InteropMessage $transportMessage): Message
+    {
+        $clientMessage = new Message();
+
+        $clientMessage->setBody($transportMessage->getBody());
+        $clientMessage->setHeaders($transportMessage->getHeaders());
+        $clientMessage->setProperties($transportMessage->getProperties());
+        $clientMessage->setMessageId($transportMessage->getMessageId());
+        $clientMessage->setTimestamp($transportMessage->getTimestamp());
+        $clientMessage->setReplyTo($transportMessage->getReplyTo());
+        $clientMessage->setCorrelationId($transportMessage->getCorrelationId());
+
+        if ($contentType = $transportMessage->getProperty(Config::CONTENT_TYPE)) {
+            $clientMessage->setContentType($contentType);
+        }
+
+        if ($priority = $transportMessage->getProperty(Config::PRIORITY)) {
+            $clientMessage->setPriority($priority);
+        }
+
+        if ($delay = $transportMessage->getProperty(Config::DELAY)) {
+            $clientMessage->setDelay((int) $delay);
+        }
+
+        if ($expire = $transportMessage->getProperty(Config::EXPIRE)) {
+            $clientMessage->setExpire((int) $expire);
+        }
+
+        return $clientMessage;
+    }
+
+    public function getConfig(): Config
+    {
+        return $this->config;
+    }
+
+    public function getContext(): Context
+    {
+        return $this->context;
+    }
+
+    public function getRouteCollection(): RouteCollection
+    {
+        return $this->routeCollection;
+    }
+
+    protected function doSendToRouter(InteropProducer $producer, Destination $topic, InteropMessage $transportMessage): void
+    {
+        $producer->send($topic, $transportMessage);
+    }
+
+    protected function doSendToProcessor(InteropProducer $producer, InteropQueue $queue, InteropMessage $transportMessage): void
+    {
+        $producer->send($queue, $transportMessage);
+    }
+
+    protected function createRouterTopic(): Destination
+    {
+        return $this->createQueue($this->getConfig()->getRouterQueue());
+    }
+
+    protected function createTransportRouterTopicName(string $name, bool $prefix): string
+    {
+        $clientPrefix = $prefix ? $this->config->getPrefix() : '';
+
+        return strtolower(implode($this->config->getSeparator(), array_filter([$clientPrefix, $name])));
+    }
+
+    protected function createTransportQueueName(string $name, bool $prefix): string
+    {
+        $clientPrefix = $prefix ? $this->config->getPrefix() : '';
+        $clientAppName = $prefix ? $this->config->getApp() : '';
+
+        return strtolower(implode($this->config->getSeparator(), array_filter([$clientPrefix, $clientAppName, $name])));
+    }
+
+    protected function doCreateQueue(string $transportQueueName): InteropQueue
+    {
+        return $this->context->createQueue($transportQueueName);
+    }
+
+    protected function doCreateTopic(string $transportTopicName): InteropTopic
+    {
+        return $this->context->createTopic($transportTopicName);
+    }
+
+    /**
+     * [client message priority => transport message priority].
+     *
+     * @return int[]
+     */
+    protected function getPriorityMap(): array
+    {
+        return [
+            MessagePriority::VERY_LOW => 0,
+            MessagePriority::LOW => 1,
+            MessagePriority::NORMAL => 2,
+            MessagePriority::HIGH => 3,
+            MessagePriority::VERY_HIGH => 4,
+        ];
+    }
+}
diff --git a/Client/Driver/GpsDriver.php b/Client/Driver/GpsDriver.php
new file mode 100644
index 0000000..32d14f7
--- /dev/null
+++ b/Client/Driver/GpsDriver.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Enqueue\Client\Driver;
+
+use Enqueue\Gps\GpsContext;
+use Enqueue\Gps\GpsQueue;
+use Enqueue\Gps\GpsTopic;
+use Interop\Queue\Destination;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * @method GpsContext getContext
+ * @method GpsQueue createQueue(string $name)
+ */
+class GpsDriver extends GenericDriver
+{
+    public function __construct(GpsContext $context, ...$args)
+    {
+        parent::__construct($context, ...$args);
+    }
+
+    public function setupBroker(?LoggerInterface $logger = null): void
+    {
+        $logger = $logger ?: new NullLogger();
+        $log = function ($text, ...$args) use ($logger) {
+            $logger->debug(sprintf('[GpsDriver] '.$text, ...$args));
+        };
+
+        // setup router
+        $routerTopic = $this->createRouterTopic();
+        $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue());
+
+        $log('Subscribe router topic to queue: %s -> %s', $routerTopic->getTopicName(), $routerQueue->getQueueName());
+        $this->getContext()->subscribe($routerTopic, $routerQueue);
+
+        // setup queues
+        $declaredQueues = [];
+        foreach ($this->getRouteCollection()->all() as $route) {
+            /** @var GpsQueue $queue */
+            $queue = $this->createRouteQueue($route);
+            if (array_key_exists($queue->getQueueName(), $declaredQueues)) {
+                continue;
+            }
+
+            $topic = $this->getContext()->createTopic($queue->getQueueName());
+
+            $log('Subscribe processor topic to queue: %s -> %s', $topic->getTopicName(), $queue->getQueueName());
+            $this->getContext()->subscribe($topic, $queue);
+
+            $declaredQueues[$queue->getQueueName()] = true;
+        }
+    }
+
+    /**
+     * @return GpsTopic
+     */
+    protected function createRouterTopic(): Destination
+    {
+        return $this->doCreateTopic(
+            $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true)
+        );
+    }
+}
diff --git a/Client/Driver/MongodbDriver.php b/Client/Driver/MongodbDriver.php
new file mode 100644
index 0000000..1c9cff4
--- /dev/null
+++ b/Client/Driver/MongodbDriver.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Enqueue\Client\Driver;
+
+use Enqueue\Mongodb\MongodbContext;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * @method MongodbContext getContext
+ */
+class MongodbDriver extends GenericDriver
+{
+    public function __construct(MongodbContext $context, ...$args)
+    {
+        parent::__construct($context, ...$args);
+    }
+
+    public function setupBroker(?LoggerInterface $logger = null): void
+    {
+        $logger = $logger ?: new NullLogger();
+        $log = function ($text, ...$args) use ($logger) {
+            $logger->debug(sprintf('[MongodbDriver] '.$text, ...$args));
+        };
+
+        $contextConfig = $this->getContext()->getConfig();
+        $log('Creating database and collection: "%s" "%s"', $contextConfig['dbname'], $contextConfig['collection_name']);
+        $this->getContext()->createCollection();
+    }
+}
diff --git a/Client/Driver/RabbitMqDriver.php b/Client/Driver/RabbitMqDriver.php
new file mode 100644
index 0000000..f215d55
--- /dev/null
+++ b/Client/Driver/RabbitMqDriver.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Enqueue\Client\Driver;
+
+use Interop\Amqp\AmqpQueue;
+use Interop\Queue\Queue as InteropQueue;
+
+class RabbitMqDriver extends AmqpDriver
+{
+    /**
+     * @return AmqpQueue
+     */
+    protected function doCreateQueue(string $transportQueueName): InteropQueue
+    {
+        $queue = parent::doCreateQueue($transportQueueName);
+        $queue->setArguments(['x-max-priority' => 4]);
+
+        return $queue;
+    }
+}
diff --git a/Client/Driver/RabbitMqStompDriver.php b/Client/Driver/RabbitMqStompDriver.php
new file mode 100644
index 0000000..7af2db8
--- /dev/null
+++ b/Client/Driver/RabbitMqStompDriver.php
@@ -0,0 +1,191 @@
+<?php
+
+namespace Enqueue\Client\Driver;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\Message;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Stomp\StompContext;
+use Enqueue\Stomp\StompDestination;
+use Enqueue\Stomp\StompMessage;
+use Enqueue\Stomp\StompProducer;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+class RabbitMqStompDriver extends StompDriver
+{
+    /**
+     * @var StompManagementClient
+     */
+    private $management;
+
+    public function __construct(StompContext $context, Config $config, RouteCollection $routeCollection, StompManagementClient $management)
+    {
+        parent::__construct($context, $config, $routeCollection);
+
+        $this->management = $management;
+    }
+
+    /**
+     * @return StompMessage
+     */
+    public function createTransportMessage(Message $message): InteropMessage
+    {
+        $transportMessage = parent::createTransportMessage($message);
+
+        if ($message->getExpire()) {
+            $transportMessage->setHeader('expiration', (string) ($message->getExpire() * 1000));
+        }
+
+        if ($priority = $message->getPriority()) {
+            $priorityMap = $this->getPriorityMap();
+
+            if (false == array_key_exists($priority, $priorityMap)) {
+                throw new \LogicException(sprintf('Cant convert client priority to transport: "%s"', $priority));
+            }
+
+            $transportMessage->setHeader('priority', $priorityMap[$priority]);
+        }
+
+        if ($message->getDelay()) {
+            if (false == $this->getConfig()->getTransportOption('delay_plugin_installed', false)) {
+                throw new \LogicException('The message delaying is not supported. In order to use delay feature install RabbitMQ delay plugin.');
+            }
+
+            $transportMessage->setHeader('x-delay', (string) ($message->getDelay() * 1000));
+        }
+
+        return $transportMessage;
+    }
+
+    public function setupBroker(?LoggerInterface $logger = null): void
+    {
+        $logger = $logger ?: new NullLogger();
+        $log = function ($text, ...$args) use ($logger) {
+            $logger->debug(sprintf('[RabbitMqStompDriver] '.$text, ...$args));
+        };
+
+        if (false == $this->getConfig()->getTransportOption('management_plugin_installed', false)) {
+            $log('Could not setup broker. The option `management_plugin_installed` is not enabled. Please enable that option and install rabbit management plugin');
+
+            return;
+        }
+
+        // setup router
+        $routerExchange = $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true);
+        $log('Declare router exchange: %s', $routerExchange);
+        $this->management->declareExchange($routerExchange, [
+            'type' => 'fanout',
+            'durable' => true,
+            'auto_delete' => false,
+        ]);
+
+        $routerQueue = $this->createTransportQueueName($this->getConfig()->getRouterQueue(), true);
+        $log('Declare router queue: %s', $routerQueue);
+        $this->management->declareQueue($routerQueue, [
+            'auto_delete' => false,
+            'durable' => true,
+            'arguments' => [
+                'x-max-priority' => 4,
+            ],
+        ]);
+
+        $log('Bind router queue to exchange: %s -> %s', $routerQueue, $routerExchange);
+        $this->management->bind($routerExchange, $routerQueue, $routerQueue);
+
+        // setup queues
+        foreach ($this->getRouteCollection()->all() as $route) {
+            $queue = $this->createRouteQueue($route);
+
+            $log('Declare processor queue: %s', $queue->getStompName());
+            $this->management->declareQueue($queue->getStompName(), [
+                'auto_delete' => false,
+                'durable' => true,
+                'arguments' => [
+                    'x-max-priority' => 4,
+                ],
+            ]);
+        }
+
+        // setup delay exchanges
+        if ($this->getConfig()->getTransportOption('delay_plugin_installed', false)) {
+            foreach ($this->getRouteCollection()->all() as $route) {
+                $queue = $this->createRouteQueue($route);
+                $delayExchange = $queue->getStompName().'.delayed';
+
+                $log('Declare delay exchange: %s', $delayExchange);
+                $this->management->declareExchange($delayExchange, [
+                    'type' => 'x-delayed-message',
+                    'durable' => true,
+                    'auto_delete' => false,
+                    'arguments' => [
+                        'x-delayed-type' => 'direct',
+                    ],
+                ]);
+
+                $log('Bind processor queue to delay exchange: %s -> %s', $queue->getStompName(), $delayExchange);
+                $this->management->bind($delayExchange, $queue->getStompName(), $queue->getStompName());
+            }
+        } else {
+            $log('Delay exchange and bindings are not setup. if you\'d like to use delays please install delay rabbitmq plugin and set delay_plugin_installed option to true');
+        }
+    }
+
+    /**
+     * @return StompDestination
+     */
+    protected function doCreateQueue(string $transportQueueName): InteropQueue
+    {
+        $queue = parent::doCreateQueue($transportQueueName);
+        $queue->setHeader('x-max-priority', 4);
+
+        return $queue;
+    }
+
+    /**
+     * @param StompProducer    $producer
+     * @param StompDestination $topic
+     * @param StompMessage     $transportMessage
+     */
+    protected function doSendToRouter(InteropProducer $producer, Destination $topic, InteropMessage $transportMessage): void
+    {
+        // We should not handle priority, expiration, and delay at this stage.
+        // The router will take care of it while re-sending the message to the final destinations.
+        $transportMessage->setHeader('expiration', null);
+        $transportMessage->setHeader('priority', null);
+        $transportMessage->setHeader('x-delay', null);
+
+        $producer->send($topic, $transportMessage);
+    }
+
+    /**
+     * @param StompProducer    $producer
+     * @param StompDestination $destination
+     * @param StompMessage     $transportMessage
+     */
+    protected function doSendToProcessor(InteropProducer $producer, InteropQueue $destination, InteropMessage $transportMessage): void
+    {
+        if ($delay = $transportMessage->getProperty(Config::DELAY)) {
+            $producer->setDeliveryDelay(null);
+            $destination = $this->createDelayedTopic($destination);
+        }
+
+        $producer->send($destination, $transportMessage);
+    }
+
+    private function createDelayedTopic(StompDestination $queue): StompDestination
+    {
+        // in order to use delay feature make sure the rabbitmq_delayed_message_exchange plugin is installed.
+        $destination = $this->getContext()->createTopic($queue->getStompName().'.delayed');
+        $destination->setType(StompDestination::TYPE_EXCHANGE);
+        $destination->setDurable(true);
+        $destination->setAutoDelete(false);
+        $destination->setRoutingKey($queue->getStompName());
+
+        return $destination;
+    }
+}
diff --git a/Client/Driver/RdKafkaDriver.php b/Client/Driver/RdKafkaDriver.php
new file mode 100644
index 0000000..2609e3f
--- /dev/null
+++ b/Client/Driver/RdKafkaDriver.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Enqueue\Client\Driver;
+
+use Enqueue\RdKafka\RdKafkaContext;
+use Enqueue\RdKafka\RdKafkaTopic;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * @method RdKafkaContext getContext()
+ */
+class RdKafkaDriver extends GenericDriver
+{
+    public function __construct(RdKafkaContext $context, ...$args)
+    {
+        parent::__construct($context, ...$args);
+    }
+
+    public function setupBroker(?LoggerInterface $logger = null): void
+    {
+        $logger = $logger ?: new NullLogger();
+        $logger->debug('[RdKafkaDriver] setup broker');
+        $log = function ($text, ...$args) use ($logger) {
+            $logger->debug(sprintf('[RdKafkaDriver] '.$text, ...$args));
+        };
+
+        // setup router
+        $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue());
+        $log('Create router queue: %s', $routerQueue->getQueueName());
+        $this->getContext()->createConsumer($routerQueue);
+
+        // setup queues
+        $declaredQueues = [];
+        foreach ($this->getRouteCollection()->all() as $route) {
+            /** @var RdKafkaTopic $queue */
+            $queue = $this->createRouteQueue($route);
+            if (array_key_exists($queue->getQueueName(), $declaredQueues)) {
+                continue;
+            }
+
+            $log('Create processor queue: %s', $queue->getQueueName());
+            $this->getContext()->createConsumer($queue);
+        }
+    }
+}
diff --git a/Client/Driver/RedisDriver.php b/Client/Driver/RedisDriver.php
new file mode 100644
index 0000000..493cb7c
--- /dev/null
+++ b/Client/Driver/RedisDriver.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Enqueue\Client\Driver;
+
+use Enqueue\Redis\RedisContext;
+use Enqueue\Redis\RedisDestination;
+
+/**
+ * @method RedisContext getContext
+ * @method RedisDestination createQueue(string $name)
+ */
+class RedisDriver extends GenericDriver
+{
+    public function __construct(RedisContext $context, ...$args)
+    {
+        parent::__construct($context, ...$args);
+    }
+}
diff --git a/Client/Driver/SnsQsDriver.php b/Client/Driver/SnsQsDriver.php
new file mode 100644
index 0000000..f4bde10
--- /dev/null
+++ b/Client/Driver/SnsQsDriver.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Enqueue\Client\Driver;
+
+use Enqueue\SnsQs\SnsQsContext;
+use Enqueue\SnsQs\SnsQsTopic;
+use Interop\Queue\Destination;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * @method SnsQsContext getContext()
+ * @method SnsQsTopic   createRouterTopic()
+ */
+class SnsQsDriver extends GenericDriver
+{
+    public function __construct(SnsQsContext $context, ...$args)
+    {
+        parent::__construct($context, ...$args);
+    }
+
+    public function setupBroker(?LoggerInterface $logger = null): void
+    {
+        $logger = $logger ?: new NullLogger();
+        $log = function ($text, ...$args) use ($logger) {
+            $logger->debug(sprintf('[SqsQsDriver] '.$text, ...$args));
+        };
+
+        // setup router
+        $routerTopic = $this->createRouterTopic();
+        $log('Declare router topic: %s', $routerTopic->getTopicName());
+        $this->getContext()->declareTopic($routerTopic);
+
+        $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue());
+        $log('Declare router queue: %s', $routerQueue->getQueueName());
+        $this->getContext()->declareQueue($routerQueue);
+
+        $log('Bind router queue to topic: %s -> %s', $routerQueue->getQueueName(), $routerTopic->getTopicName());
+        $this->getContext()->bind($routerTopic, $routerQueue);
+
+        // setup queues
+        $declaredQueues = [];
+        $declaredTopics = [];
+        foreach ($this->getRouteCollection()->all() as $route) {
+            $queue = $this->createRouteQueue($route);
+            if (false === array_key_exists($queue->getQueueName(), $declaredQueues)) {
+                $log('Declare processor queue: %s', $queue->getQueueName());
+                $this->getContext()->declareQueue($queue);
+
+                $declaredQueues[$queue->getQueueName()] = true;
+            }
+
+            if ($route->isCommand()) {
+                continue;
+            }
+
+            $topic = $this->doCreateTopic($this->createTransportQueueName($route->getSource(), true));
+            if (false === array_key_exists($topic->getTopicName(), $declaredTopics)) {
+                $log('Declare processor topic: %s', $topic->getTopicName());
+                $this->getContext()->declareTopic($topic);
+
+                $declaredTopics[$topic->getTopicName()] = true;
+            }
+
+            $log('Bind processor queue to topic: %s -> %s', $queue->getQueueName(), $topic->getTopicName());
+            $this->getContext()->bind($topic, $queue);
+        }
+    }
+
+    protected function createRouterTopic(): Destination
+    {
+        return $this->doCreateTopic(
+            $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true)
+        );
+    }
+
+    protected function createTransportRouterTopicName(string $name, bool $prefix): string
+    {
+        $name = parent::createTransportRouterTopicName($name, $prefix);
+
+        return str_replace('.', '_dot_', $name);
+    }
+
+    protected function createTransportQueueName(string $name, bool $prefix): string
+    {
+        $name = parent::createTransportQueueName($name, $prefix);
+
+        return str_replace('.', '_dot_', $name);
+    }
+}
diff --git a/Client/Driver/SqsDriver.php b/Client/Driver/SqsDriver.php
new file mode 100644
index 0000000..49b696a
--- /dev/null
+++ b/Client/Driver/SqsDriver.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Enqueue\Client\Driver;
+
+use Enqueue\Sqs\SqsContext;
+use Enqueue\Sqs\SqsDestination;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * @method SqsContext getContext
+ * @method SqsDestination createQueue(string $clientQueueName): InteropQueue
+ */
+class SqsDriver extends GenericDriver
+{
+    public function __construct(SqsContext $context, ...$args)
+    {
+        parent::__construct($context, ...$args);
+    }
+
+    public function setupBroker(?LoggerInterface $logger = null): void
+    {
+        $logger = $logger ?: new NullLogger();
+        $log = function ($text, ...$args) use ($logger) {
+            $logger->debug(sprintf('[SqsDriver] '.$text, ...$args));
+        };
+
+        // setup router
+        $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue());
+        $log('Declare router queue: %s', $routerQueue->getQueueName());
+        $this->getContext()->declareQueue($routerQueue);
+
+        // setup queues
+        $declaredQueues = [];
+        foreach ($this->getRouteCollection()->all() as $route) {
+            /** @var SqsDestination $queue */
+            $queue = $this->createRouteQueue($route);
+            if (array_key_exists($queue->getQueueName(), $declaredQueues)) {
+                continue;
+            }
+
+            $log('Declare processor queue: %s', $queue->getQueueName());
+            $this->getContext()->declareQueue($queue);
+
+            $declaredQueues[$queue->getQueueName()] = true;
+        }
+    }
+
+    protected function createTransportRouterTopicName(string $name, bool $prefix): string
+    {
+        $name = parent::createTransportRouterTopicName($name, $prefix);
+
+        return str_replace('.', '_dot_', $name);
+    }
+
+    protected function createTransportQueueName(string $name, bool $prefix): string
+    {
+        $name = parent::createTransportQueueName($name, $prefix);
+
+        return str_replace('.', '_dot_', $name);
+    }
+}
diff --git a/Client/Driver/StompDriver.php b/Client/Driver/StompDriver.php
new file mode 100644
index 0000000..811ad76
--- /dev/null
+++ b/Client/Driver/StompDriver.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Enqueue\Client\Driver;
+
+use Enqueue\Client\Message;
+use Enqueue\Stomp\StompContext;
+use Enqueue\Stomp\StompDestination;
+use Enqueue\Stomp\StompMessage;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Queue as InteropQueue;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * @method StompContext getContext
+ */
+class StompDriver extends GenericDriver
+{
+    public function __construct(StompContext $context, ...$args)
+    {
+        parent::__construct($context, ...$args);
+    }
+
+    public function setupBroker(?LoggerInterface $logger = null): void
+    {
+        $logger = $logger ?: new NullLogger();
+        $logger->debug('[StompDriver] Stomp protocol does not support broker configuration');
+    }
+
+    /**
+     * @return StompMessage
+     */
+    public function createTransportMessage(Message $message): InteropMessage
+    {
+        /** @var StompMessage $transportMessage */
+        $transportMessage = parent::createTransportMessage($message);
+        $transportMessage->setPersistent(true);
+
+        return $transportMessage;
+    }
+
+    /**
+     * @return StompDestination
+     */
+    protected function doCreateQueue(string $transportQueueName): InteropQueue
+    {
+        /** @var StompDestination $queue */
+        $queue = parent::doCreateQueue($transportQueueName);
+        $queue->setDurable(true);
+        $queue->setAutoDelete(false);
+        $queue->setExclusive(false);
+
+        return $queue;
+    }
+
+    /**
+     * @return StompDestination
+     */
+    protected function createRouterTopic(): Destination
+    {
+        /** @var StompDestination $topic */
+        $topic = $this->doCreateTopic(
+            $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true)
+        );
+        $topic->setDurable(true);
+        $topic->setAutoDelete(false);
+
+        return $topic;
+    }
+}
diff --git a/Client/Driver/StompManagementClient.php b/Client/Driver/StompManagementClient.php
new file mode 100644
index 0000000..0d64450
--- /dev/null
+++ b/Client/Driver/StompManagementClient.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Enqueue\Client\Driver;
+
+use RabbitMq\ManagementApi\Client;
+
+class StompManagementClient
+{
+    /**
+     * @var Client
+     */
+    private $client;
+
+    /**
+     * @var string
+     */
+    private $vhost;
+
+    public function __construct(Client $client, string $vhost = '/')
+    {
+        $this->client = $client;
+        $this->vhost = $vhost;
+    }
+
+    public static function create(string $vhost = '/', string $host = 'localhost', int $port = 15672, string $login = 'guest', string $password = 'guest'): self
+    {
+        return new self(new Client(null, 'http://'.$host.':'.$port, $login, $password), $vhost);
+    }
+
+    public function declareQueue(string $name, array $options)
+    {
+        return $this->client->queues()->create($this->vhost, $name, $options);
+    }
+
+    public function declareExchange(string $name, array $options)
+    {
+        return $this->client->exchanges()->create($this->vhost, $name, $options);
+    }
+
+    public function bind(string $exchange, string $queue, ?string $routingKey = null, $arguments = null)
+    {
+        return $this->client->bindings()->create($this->vhost, $exchange, $queue, $routingKey, $arguments);
+    }
+}
diff --git a/Client/DriverFactory.php b/Client/DriverFactory.php
new file mode 100644
index 0000000..5c827e7
--- /dev/null
+++ b/Client/DriverFactory.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Enqueue\Client;
+
+use Enqueue\Client\Driver\RabbitMqStompDriver;
+use Enqueue\Client\Driver\StompManagementClient;
+use Enqueue\Dsn\Dsn;
+use Interop\Queue\ConnectionFactory;
+
+final class DriverFactory implements DriverFactoryInterface
+{
+    public function create(ConnectionFactory $factory, Config $config, RouteCollection $collection): DriverInterface
+    {
+        $dsn = $config->getTransportOption('dsn');
+
+        if (empty($dsn)) {
+            throw new \LogicException('This driver factory relies on dsn option from transport config. The option is empty or not set.');
+        }
+
+        $dsn = Dsn::parseFirst($dsn);
+
+        if ($driverInfo = $this->findDriverInfo($dsn, Resources::getAvailableDrivers())) {
+            $driverClass = $driverInfo['driverClass'];
+
+            if (RabbitMqStompDriver::class === $driverClass) {
+                return $this->createRabbitMqStompDriver($factory, $dsn, $config, $collection);
+            }
+
+            return new $driverClass($factory->createContext(), $config, $collection);
+        }
+
+        $knownDrivers = Resources::getKnownDrivers();
+        if ($driverInfo = $this->findDriverInfo($dsn, $knownDrivers)) {
+            throw new \LogicException(sprintf('To use given scheme "%s" a package has to be installed. Run "composer req %s" to add it.', $dsn->getScheme(), implode(' ', $driverInfo['packages'])));
+        }
+
+        throw new \LogicException(sprintf('A given scheme "%s" is not supported. Maybe it is a custom driver, make sure you registered it with "%s::addDriver".', $dsn->getScheme(), Resources::class));
+    }
+
+    private function findDriverInfo(Dsn $dsn, array $factories): ?array
+    {
+        $protocol = $dsn->getSchemeProtocol();
+
+        if ($dsn->getSchemeExtensions()) {
+            foreach ($factories as $info) {
+                if (empty($info['requiredSchemeExtensions'])) {
+                    continue;
+                }
+
+                if (false == in_array($protocol, $info['schemes'], true)) {
+                    continue;
+                }
+
+                $diff = array_diff($dsn->getSchemeExtensions(), $info['requiredSchemeExtensions']);
+                if (empty($diff)) {
+                    return $info;
+                }
+            }
+        }
+
+        foreach ($factories as $driverClass => $info) {
+            if (false == empty($info['requiredSchemeExtensions'])) {
+                continue;
+            }
+
+            if (false == in_array($protocol, $info['schemes'], true)) {
+                continue;
+            }
+
+            return $info;
+        }
+
+        return null;
+    }
+
+    private function createRabbitMqStompDriver(ConnectionFactory $factory, Dsn $dsn, Config $config, RouteCollection $collection): RabbitMqStompDriver
+    {
+        $defaultManagementHost = $dsn->getHost() ?: $config->getTransportOption('host', 'localhost');
+        $managementVast = ltrim($dsn->getPath() ?? '', '/') ?: $config->getTransportOption('vhost', '/');
+
+        $managementClient = StompManagementClient::create(
+            urldecode($managementVast),
+            $config->getDriverOption('rabbitmq_management_host', $defaultManagementHost),
+            $config->getDriverOption('rabbitmq_management_port', 15672),
+            (string) $dsn->getUser() ?: $config->getTransportOption('user', 'guest'),
+            (string) $dsn->getPassword() ?: $config->getTransportOption('pass', 'guest')
+        );
+
+        return new RabbitMqStompDriver($factory->createContext(), $config, $collection, $managementClient);
+    }
+}
diff --git a/Client/DriverFactoryInterface.php b/Client/DriverFactoryInterface.php
new file mode 100644
index 0000000..698ad05
--- /dev/null
+++ b/Client/DriverFactoryInterface.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Enqueue\Client;
+
+use Interop\Queue\ConnectionFactory;
+
+interface DriverFactoryInterface
+{
+    public function create(ConnectionFactory $factory, Config $config, RouteCollection $collection): DriverInterface;
+}
diff --git a/Client/DriverInterface.php b/Client/DriverInterface.php
index 2bb0cdc..9d1dde6 100644
--- a/Client/DriverInterface.php
+++ b/Client/DriverInterface.php
@@ -1,53 +1,37 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Enqueue\Client;
 
-use Interop\Queue\PsrMessage;
-use Interop\Queue\PsrQueue;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Queue as InteropQueue;
 use Psr\Log\LoggerInterface;
 
 interface DriverInterface
 {
-    /**
-     * @param Message $message
-     *
-     * @return PsrMessage
-     */
-    public function createTransportMessage(Message $message);
+    public function createTransportMessage(Message $message): InteropMessage;
 
-    /**
-     * @param PsrMessage $message
-     *
-     * @return Message
-     */
-    public function createClientMessage(PsrMessage $message);
+    public function createClientMessage(InteropMessage $message): Message;
 
-    /**
-     * @param Message $message
-     */
-    public function sendToRouter(Message $message);
+    public function sendToRouter(Message $message): DriverSendResult;
 
-    /**
-     * @param Message $message
-     */
-    public function sendToProcessor(Message $message);
+    public function sendToProcessor(Message $message): DriverSendResult;
 
-    /**
-     * @param string $queueName
-     *
-     * @return PsrQueue
-     */
-    public function createQueue($queueName);
+    public function createQueue(string $queueName, bool $prefix = true): InteropQueue;
 
-    /**
-     * Creates all required queues, exchanges, topics, bindings on broker side.
-     *
-     * @param LoggerInterface $logger
-     */
-    public function setupBroker(LoggerInterface $logger = null);
+    public function createRouteQueue(Route $route): InteropQueue;
 
     /**
-     * @return Config
+     * Prepare broker for work.
+     * Creates all required queues, exchanges, topics, bindings etc.
      */
-    public function getConfig();
+    public function setupBroker(?LoggerInterface $logger = null): void;
+
+    public function getConfig(): Config;
+
+    public function getContext(): Context;
+
+    public function getRouteCollection(): RouteCollection;
 }
diff --git a/Client/DriverPreSend.php b/Client/DriverPreSend.php
new file mode 100644
index 0000000..cae615d
--- /dev/null
+++ b/Client/DriverPreSend.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Enqueue\Client;
+
+final class DriverPreSend
+{
+    private $message;
+
+    private $producer;
+
+    private $driver;
+
+    public function __construct(Message $message, ProducerInterface $producer, DriverInterface $driver)
+    {
+        $this->message = $message;
+        $this->producer = $producer;
+        $this->driver = $driver;
+    }
+
+    public function getMessage(): Message
+    {
+        return $this->message;
+    }
+
+    public function getProducer(): ProducerInterface
+    {
+        return $this->producer;
+    }
+
+    public function getDriver(): DriverInterface
+    {
+        return $this->driver;
+    }
+
+    public function isEvent(): bool
+    {
+        return (bool) $this->message->getProperty(Config::TOPIC);
+    }
+
+    public function isCommand(): bool
+    {
+        return (bool) $this->message->getProperty(Config::COMMAND);
+    }
+
+    public function getCommand(): string
+    {
+        return $this->message->getProperty(Config::COMMAND);
+    }
+
+    public function getTopic(): string
+    {
+        return $this->message->getProperty(Config::TOPIC);
+    }
+}
diff --git a/Client/DriverPreSendExtensionInterface.php b/Client/DriverPreSendExtensionInterface.php
new file mode 100644
index 0000000..fd95c93
--- /dev/null
+++ b/Client/DriverPreSendExtensionInterface.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Enqueue\Client;
+
+interface DriverPreSendExtensionInterface
+{
+    public function onDriverPreSend(DriverPreSend $context): void;
+}
diff --git a/Client/DriverSendResult.php b/Client/DriverSendResult.php
new file mode 100644
index 0000000..d92e28d
--- /dev/null
+++ b/Client/DriverSendResult.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Enqueue\Client;
+
+use Interop\Queue\Destination;
+use Interop\Queue\Message as TransportMessage;
+
+final class DriverSendResult
+{
+    /**
+     * @var Destination
+     */
+    private $transportDestination;
+
+    /**
+     * @var TransportMessage
+     */
+    private $transportMessage;
+
+    public function __construct(Destination $transportDestination, TransportMessage $transportMessage)
+    {
+        $this->transportDestination = $transportDestination;
+        $this->transportMessage = $transportMessage;
+    }
+
+    public function getTransportDestination(): Destination
+    {
+        return $this->transportDestination;
+    }
+
+    public function getTransportMessage(): TransportMessage
+    {
+        return $this->transportMessage;
+    }
+}
diff --git a/Client/Extension/PrepareBodyExtension.php b/Client/Extension/PrepareBodyExtension.php
new file mode 100644
index 0000000..e792454
--- /dev/null
+++ b/Client/Extension/PrepareBodyExtension.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Enqueue\Client\Extension;
+
+use Enqueue\Client\Message;
+use Enqueue\Client\PreSend;
+use Enqueue\Client\PreSendCommandExtensionInterface;
+use Enqueue\Client\PreSendEventExtensionInterface;
+use Enqueue\Util\JSON;
+
+class PrepareBodyExtension implements PreSendEventExtensionInterface, PreSendCommandExtensionInterface
+{
+    public function onPreSendEvent(PreSend $context): void
+    {
+        $this->prepareBody($context->getMessage());
+    }
+
+    public function onPreSendCommand(PreSend $context): void
+    {
+        $this->prepareBody($context->getMessage());
+    }
+
+    private function prepareBody(Message $message): void
+    {
+        $body = $message->getBody();
+        $contentType = $message->getContentType();
+
+        if (is_scalar($body) || null === $body) {
+            $contentType = $contentType ?: 'text/plain';
+            $body = (string) $body;
+        } elseif (is_array($body)) {
+            // only array of scalars is allowed.
+            array_walk_recursive($body, function ($value) {
+                if (!is_scalar($value) && null !== $value) {
+                    throw new \LogicException(sprintf('The message\'s body must be an array of scalars. Found not scalar in the array: %s', is_object($value) ? $value::class : gettype($value)));
+                }
+            });
+
+            $contentType = $contentType ?: 'application/json';
+            $body = JSON::encode($body);
+        } elseif ($body instanceof \JsonSerializable) {
+            $contentType = $contentType ?: 'application/json';
+            $body = JSON::encode($body);
+        } else {
+            throw new \InvalidArgumentException(sprintf('The message\'s body must be either null, scalar, array or object (implements \JsonSerializable). Got: %s', is_object($body) ? $body::class : gettype($body)));
+        }
+
+        $message->setContentType($contentType);
+        $message->setBody($body);
+    }
+}
diff --git a/Client/ExtensionInterface.php b/Client/ExtensionInterface.php
index 4f9fd66..596b1b9 100644
--- a/Client/ExtensionInterface.php
+++ b/Client/ExtensionInterface.php
@@ -2,21 +2,6 @@
 
 namespace Enqueue\Client;
 
-interface ExtensionInterface
+interface ExtensionInterface extends PreSendEventExtensionInterface, PreSendCommandExtensionInterface, DriverPreSendExtensionInterface, PostSendExtensionInterface
 {
-    /**
-     * @param string  $topic
-     * @param Message $message
-     *
-     * @return
-     */
-    public function onPreSend($topic, Message $message);
-
-    /**
-     * @param string  $topic
-     * @param Message $message
-     *
-     * @return
-     */
-    public function onPostSend($topic, Message $message);
 }
diff --git a/Client/Message.php b/Client/Message.php
index d58371b..7e51ea1 100644
--- a/Client/Message.php
+++ b/Client/Message.php
@@ -7,12 +7,12 @@ class Message
     /**
      * @const string
      */
-    const SCOPE_MESSAGE_BUS = 'enqueue.scope.message_bus';
+    public const SCOPE_MESSAGE_BUS = 'enqueue.scope.message_bus';
 
     /**
      * @const string
      */
-    const SCOPE_APP = 'enqueue.scope.app';
+    public const SCOPE_APP = 'enqueue.scope.app';
 
     /**
      * @var string|null
@@ -88,7 +88,7 @@ public function __construct($body = '', array $properties = [], array $headers =
     }
 
     /**
-     * @return null|string
+     * @return string|null
      */
     public function getBody()
     {
@@ -96,7 +96,7 @@ public function getBody()
     }
 
     /**
-     * @param null|string|int|float|array|\JsonSerializable $body
+     * @param string|int|float|array|\JsonSerializable|null $body
      */
     public function setBody($body)
     {
@@ -205,18 +205,12 @@ public function setDelay($delay)
         $this->delay = $delay;
     }
 
-    /**
-     * @param string $scope
-     */
-    public function setScope($scope)
+    public function setScope(string $scope): void
     {
         $this->scope = $scope;
     }
 
-    /**
-     * @return string
-     */
-    public function getScope()
+    public function getScope(): string
     {
         return $this->scope;
     }
@@ -262,10 +256,8 @@ public function getHeaders()
     }
 
     /**
-     * @param string $name
-     * @param mixed  $default
-     *
-     * @return mixed
+     * @param string     $name
+     * @param mixed|null $default
      */
     public function getHeader($name, $default = null)
     {
@@ -274,16 +266,12 @@ public function getHeader($name, $default = null)
 
     /**
      * @param string $name
-     * @param mixed  $value
      */
     public function setHeader($name, $value)
     {
         $this->headers[$name] = $value;
     }
 
-    /**
-     * @param array $headers
-     */
     public function setHeaders(array $headers)
     {
         $this->headers = $headers;
@@ -297,19 +285,14 @@ public function getProperties()
         return $this->properties;
     }
 
-    /**
-     * @param array $properties
-     */
     public function setProperties(array $properties)
     {
         $this->properties = $properties;
     }
 
     /**
-     * @param string $name
-     * @param mixed  $default
-     *
-     * @return mixed
+     * @param string     $name
+     * @param mixed|null $default
      */
     public function getProperty($name, $default = null)
     {
@@ -318,7 +301,6 @@ public function getProperty($name, $default = null)
 
     /**
      * @param string $name
-     * @param mixed  $value
      */
     public function setProperty($name, $value)
     {
diff --git a/Client/MessagePriority.php b/Client/MessagePriority.php
index efa658c..e14be9a 100644
--- a/Client/MessagePriority.php
+++ b/Client/MessagePriority.php
@@ -4,9 +4,9 @@
 
 class MessagePriority
 {
-    const VERY_LOW = 'enqueue.message_queue.client.very_low_message_priority';
-    const LOW = 'enqueue.message_queue.client.low_message_priority';
-    const NORMAL = 'enqueue.message_queue.client.normal_message_priority';
-    const HIGH = 'enqueue.message_queue.client.high_message_priority';
-    const VERY_HIGH = 'enqueue.message_queue.client.very_high_message_priority';
+    public const VERY_LOW = 'enqueue.message_queue.client.very_low_message_priority';
+    public const LOW = 'enqueue.message_queue.client.low_message_priority';
+    public const NORMAL = 'enqueue.message_queue.client.normal_message_priority';
+    public const HIGH = 'enqueue.message_queue.client.high_message_priority';
+    public const VERY_HIGH = 'enqueue.message_queue.client.very_high_message_priority';
 }
diff --git a/Client/Meta/QueueMeta.php b/Client/Meta/QueueMeta.php
deleted file mode 100644
index bee32bd..0000000
--- a/Client/Meta/QueueMeta.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-namespace Enqueue\Client\Meta;
-
-class QueueMeta
-{
-    /**
-     * @var string
-     */
-    private $clientName;
-
-    /**
-     * @var string
-     */
-    private $transportName;
-
-    /**
-     * @var string[]
-     */
-    private $processors;
-
-    /**
-     * @param string   $clientName
-     * @param string   $transportName
-     * @param string[] $processors
-     */
-    public function __construct($clientName, $transportName, array $processors = [])
-    {
-        $this->clientName = $clientName;
-        $this->transportName = $transportName;
-        $this->processors = $processors;
-    }
-
-    /**
-     * @return string
-     */
-    public function getClientName()
-    {
-        return $this->clientName;
-    }
-
-    /**
-     * @return string
-     */
-    public function getTransportName()
-    {
-        return $this->transportName;
-    }
-
-    /**
-     * @return string[]
-     */
-    public function getProcessors()
-    {
-        return $this->processors;
-    }
-}
diff --git a/Client/Meta/QueueMetaRegistry.php b/Client/Meta/QueueMetaRegistry.php
deleted file mode 100644
index 29c2d69..0000000
--- a/Client/Meta/QueueMetaRegistry.php
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php
-
-namespace Enqueue\Client\Meta;
-
-use Enqueue\Client\Config;
-
-class QueueMetaRegistry
-{
-    /**
-     * @var array
-     */
-    private $meta;
-
-    /**
-     * @var Config
-     */
-    private $config;
-
-    /**
-     * $meta = [
-     *   'aQueueName' => [
-     *     'transportName' => 'aTransportQueueName',
-     *     'processors' => ['aFooProcessorName', 'aBarProcessorName'],
-     *   ]
-     * ].
-     *
-     *
-     * @param Config $config
-     * @param array  $meta
-     */
-    public function __construct(Config $config, array $meta)
-    {
-        $this->config = $config;
-        $this->meta = $meta;
-    }
-
-    /**
-     * @param string      $queueName
-     * @param string|null $transportName
-     */
-    public function add($queueName, $transportName = null)
-    {
-        $this->meta[$queueName] = [
-            'transportName' => $transportName,
-            'processors' => [],
-        ];
-    }
-
-    /**
-     * @param string $queueName
-     * @param string $processorName
-     */
-    public function addProcessor($queueName, $processorName)
-    {
-        if (false == array_key_exists($queueName, $this->meta)) {
-            $this->add($queueName);
-        }
-
-        $this->meta[$queueName]['processors'][] = $processorName;
-    }
-
-    /**
-     * @param string $queueName
-     *
-     * @return QueueMeta
-     */
-    public function getQueueMeta($queueName)
-    {
-        if (false == array_key_exists($queueName, $this->meta)) {
-            throw new \InvalidArgumentException(sprintf(
-                'The queue meta not found. Requested name `%s`',
-                $queueName
-            ));
-        }
-
-        $transportName = $this->config->createTransportQueueName($queueName);
-
-        $meta = array_replace([
-            'processors' => [],
-            'transportName' => $transportName,
-        ], array_filter($this->meta[$queueName]));
-
-        return new QueueMeta($queueName, $meta['transportName'], $meta['processors']);
-    }
-
-    /**
-     * @return \Generator|QueueMeta[]
-     */
-    public function getQueuesMeta()
-    {
-        foreach (array_keys($this->meta) as $queueName) {
-            yield $this->getQueueMeta($queueName);
-        }
-    }
-}
diff --git a/Client/Meta/TopicMeta.php b/Client/Meta/TopicMeta.php
deleted file mode 100644
index abb0e33..0000000
--- a/Client/Meta/TopicMeta.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-namespace Enqueue\Client\Meta;
-
-class TopicMeta
-{
-    /**
-     * @var string
-     */
-    private $name;
-
-    /**
-     * @var string
-     */
-    private $description;
-
-    /**
-     * @var string[]
-     */
-    private $processors;
-
-    /**
-     * @param string   $name
-     * @param string   $description
-     * @param string[] $processors
-     */
-    public function __construct($name, $description = '', array $processors = [])
-    {
-        $this->name = $name;
-        $this->description = $description;
-        $this->processors = $processors;
-    }
-
-    /**
-     * @return string
-     */
-    public function getName()
-    {
-        return $this->name;
-    }
-
-    /**
-     * @return string
-     */
-    public function getDescription()
-    {
-        return $this->description;
-    }
-
-    /**
-     * @return string[]
-     */
-    public function getProcessors()
-    {
-        return $this->processors;
-    }
-}
diff --git a/Client/Meta/TopicMetaRegistry.php b/Client/Meta/TopicMetaRegistry.php
deleted file mode 100644
index efceb9a..0000000
--- a/Client/Meta/TopicMetaRegistry.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php
-
-namespace Enqueue\Client\Meta;
-
-class TopicMetaRegistry
-{
-    /**
-     * @var array
-     */
-    protected $meta;
-
-    /**
-     * $meta = [
-     *   'aTopicName' => [
-     *     'description' => 'A desc',
-     *     'processors' => ['aProcessorNameFoo', 'aProcessorNameBar],
-     *   ],
-     * ].
-     *
-     * @param array $meta
-     */
-    public function __construct(array $meta)
-    {
-        $this->meta = $meta;
-    }
-
-    /**
-     * @param string $topicName
-     * @param string $description
-     */
-    public function add($topicName, $description = null)
-    {
-        $this->meta[$topicName] = [
-            'description' => $description,
-            'processors' => [],
-        ];
-    }
-
-    /**
-     * @param string $topicName
-     * @param string $processorName
-     */
-    public function addProcessor($topicName, $processorName)
-    {
-        if (false == array_key_exists($topicName, $this->meta)) {
-            $this->add($topicName);
-        }
-
-        $this->meta[$topicName]['processors'][] = $processorName;
-    }
-
-    /**
-     * @param string $topicName
-     *
-     * @return TopicMeta
-     */
-    public function getTopicMeta($topicName)
-    {
-        if (false == array_key_exists($topicName, $this->meta)) {
-            throw new \InvalidArgumentException(sprintf('The topic meta not found. Requested name `%s`', $topicName));
-        }
-
-        $topic = array_replace([
-            'description' => '',
-            'processors' => [],
-        ], $this->meta[$topicName]);
-
-        return new TopicMeta($topicName, $topic['description'], $topic['processors']);
-    }
-
-    /**
-     * @return \Generator|TopicMeta[]
-     */
-    public function getTopicsMeta()
-    {
-        foreach (array_keys($this->meta) as $topicName) {
-            yield $this->getTopicMeta($topicName);
-        }
-    }
-}
diff --git a/Client/PostSend.php b/Client/PostSend.php
new file mode 100644
index 0000000..5d9526e
--- /dev/null
+++ b/Client/PostSend.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Enqueue\Client;
+
+use Interop\Queue\Destination;
+use Interop\Queue\Message as TransportMessage;
+
+final class PostSend
+{
+    private $message;
+
+    private $producer;
+
+    private $driver;
+
+    private $transportDestination;
+
+    private $transportMessage;
+
+    public function __construct(
+        Message $message,
+        ProducerInterface $producer,
+        DriverInterface $driver,
+        Destination $transportDestination,
+        TransportMessage $transportMessage,
+    ) {
+        $this->message = $message;
+        $this->producer = $producer;
+        $this->driver = $driver;
+        $this->transportDestination = $transportDestination;
+        $this->transportMessage = $transportMessage;
+    }
+
+    public function getMessage(): Message
+    {
+        return $this->message;
+    }
+
+    public function getProducer(): ProducerInterface
+    {
+        return $this->producer;
+    }
+
+    public function getDriver(): DriverInterface
+    {
+        return $this->driver;
+    }
+
+    public function getTransportDestination(): Destination
+    {
+        return $this->transportDestination;
+    }
+
+    public function getTransportMessage(): TransportMessage
+    {
+        return $this->transportMessage;
+    }
+
+    public function isEvent(): bool
+    {
+        return (bool) $this->message->getProperty(Config::TOPIC);
+    }
+
+    public function isCommand(): bool
+    {
+        return (bool) $this->message->getProperty(Config::COMMAND);
+    }
+
+    public function getCommand(): string
+    {
+        return $this->message->getProperty(Config::COMMAND);
+    }
+
+    public function getTopic(): string
+    {
+        return $this->message->getProperty(Config::TOPIC);
+    }
+}
diff --git a/Client/PostSendExtensionInterface.php b/Client/PostSendExtensionInterface.php
new file mode 100644
index 0000000..dd3ca8b
--- /dev/null
+++ b/Client/PostSendExtensionInterface.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Enqueue\Client;
+
+interface PostSendExtensionInterface
+{
+    public function onPostSend(PostSend $context): void;
+}
diff --git a/Client/PreSend.php b/Client/PreSend.php
new file mode 100644
index 0000000..b60f90b
--- /dev/null
+++ b/Client/PreSend.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Enqueue\Client;
+
+final class PreSend
+{
+    private $message;
+
+    private $originalMessage;
+
+    private $commandOrTopic;
+
+    private $producer;
+
+    private $driver;
+
+    public function __construct(
+        string $commandOrTopic,
+        Message $message,
+        ProducerInterface $producer,
+        DriverInterface $driver,
+    ) {
+        $this->message = $message;
+        $this->commandOrTopic = $commandOrTopic;
+        $this->producer = $producer;
+        $this->driver = $driver;
+
+        $this->originalMessage = clone $message;
+    }
+
+    public function getCommand(): string
+    {
+        return $this->commandOrTopic;
+    }
+
+    public function getTopic(): string
+    {
+        return $this->commandOrTopic;
+    }
+
+    public function changeCommand(string $newCommand): void
+    {
+        $this->commandOrTopic = $newCommand;
+    }
+
+    public function changeTopic(string $newTopic): void
+    {
+        $this->commandOrTopic = $newTopic;
+    }
+
+    public function changeBody($body, ?string $contentType = null): void
+    {
+        $this->message->setBody($body);
+
+        if (null !== $contentType) {
+            $this->message->setContentType($contentType);
+        }
+    }
+
+    public function getMessage(): Message
+    {
+        return $this->message;
+    }
+
+    public function getOriginalMessage(): Message
+    {
+        return $this->originalMessage;
+    }
+
+    public function getProducer(): ProducerInterface
+    {
+        return $this->producer;
+    }
+
+    public function getDriver(): DriverInterface
+    {
+        return $this->driver;
+    }
+}
diff --git a/Client/PreSendCommandExtensionInterface.php b/Client/PreSendCommandExtensionInterface.php
new file mode 100644
index 0000000..cefec09
--- /dev/null
+++ b/Client/PreSendCommandExtensionInterface.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Enqueue\Client;
+
+interface PreSendCommandExtensionInterface
+{
+    /**
+     * @throws \Exception
+     */
+    public function onPreSendCommand(PreSend $context): void;
+}
diff --git a/Client/PreSendEventExtensionInterface.php b/Client/PreSendEventExtensionInterface.php
new file mode 100644
index 0000000..ecb0519
--- /dev/null
+++ b/Client/PreSendEventExtensionInterface.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Enqueue\Client;
+
+interface PreSendEventExtensionInterface
+{
+    /**
+     * @throws \Exception
+     */
+    public function onPreSendEvent(PreSend $context): void;
+}
diff --git a/Client/ProcessorRegistryInterface.php b/Client/ProcessorRegistryInterface.php
deleted file mode 100644
index c83c113..0000000
--- a/Client/ProcessorRegistryInterface.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Enqueue\Client;
-
-use Interop\Queue\PsrProcessor;
-
-interface ProcessorRegistryInterface
-{
-    /**
-     * @param string $processorName
-     *
-     * @return PsrProcessor
-     */
-    public function get($processorName);
-}
diff --git a/Client/Producer.php b/Client/Producer.php
index a3dfacf..db50744 100644
--- a/Client/Producer.php
+++ b/Client/Producer.php
@@ -2,16 +2,17 @@
 
 namespace Enqueue\Client;
 
+use Enqueue\Client\Extension\PrepareBodyExtension;
+use Enqueue\Rpc\Promise;
 use Enqueue\Rpc\RpcFactory;
-use Enqueue\Util\JSON;
 use Enqueue\Util\UUID;
 
-class Producer implements ProducerInterface
+final class Producer implements ProducerInterface
 {
     /**
      * @var DriverInterface
      */
-    protected $driver;
+    private $driver;
 
     /**
      * @var ExtensionInterface
@@ -23,86 +24,47 @@ class Producer implements ProducerInterface
      */
     private $rpcFactory;
 
-    /**
-     * @param DriverInterface         $driver
-     * @param ExtensionInterface|null $extension
-     * @param RpcFactory              $rpcFactory
-     *
-     * @internal param RpcClient $rpcClient
-     */
     public function __construct(
         DriverInterface $driver,
         RpcFactory $rpcFactory,
-        ExtensionInterface $extension = null
+        ?ExtensionInterface $extension = null,
     ) {
         $this->driver = $driver;
         $this->rpcFactory = $rpcFactory;
-        $this->extension = $extension ?: new ChainExtension([]);
+
+        $this->extension = $extension ?
+            new ChainExtension([$extension, new PrepareBodyExtension()]) :
+            new ChainExtension([new PrepareBodyExtension()])
+        ;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function sendEvent($topic, $message)
+    public function sendEvent(string $topic, $message): void
     {
         if (false == $message instanceof Message) {
-            $body = $message;
-            $message = new Message();
-            $message->setBody($body);
-        }
-
-        $this->prepareBody($message);
-
-        $message->setProperty(Config::PARAMETER_TOPIC_NAME, $topic);
-
-        if (!$message->getMessageId()) {
-            $message->setMessageId(UUID::generate());
-        }
-
-        if (!$message->getTimestamp()) {
-            $message->setTimestamp(time());
+            $message = new Message($message);
         }
 
-        if (!$message->getPriority()) {
-            $message->setPriority(MessagePriority::NORMAL);
-        }
+        $preSend = new PreSend($topic, $message, $this, $this->driver);
+        $this->extension->onPreSendEvent($preSend);
 
-        if (Message::SCOPE_MESSAGE_BUS == $message->getScope()) {
-            if ($message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) {
-                throw new \LogicException(sprintf('The %s property must not be set for messages that are sent to message bus.', Config::PARAMETER_PROCESSOR_QUEUE_NAME));
-            }
-            if ($message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) {
-                throw new \LogicException(sprintf('The %s property must not be set for messages that are sent to message bus.', Config::PARAMETER_PROCESSOR_NAME));
-            }
+        $message = $preSend->getMessage();
+        $message->setProperty(Config::TOPIC, $preSend->getTopic());
 
-            $this->extension->onPreSend($topic, $message);
-            $this->driver->sendToRouter($message);
-            $this->extension->onPostSend($topic, $message);
-        } elseif (Message::SCOPE_APP == $message->getScope()) {
-            if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) {
-                $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, $this->driver->getConfig()->getRouterProcessorName());
-            }
-            if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) {
-                $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $this->driver->getConfig()->getRouterQueueName());
-            }
-
-            $this->extension->onPreSend($topic, $message);
-            $this->driver->sendToProcessor($message);
-            $this->extension->onPostSend($topic, $message);
-        } else {
-            throw new \LogicException(sprintf('The message scope "%s" is not supported.', $message->getScope()));
-        }
+        $this->doSend($message);
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function sendCommand($command, $message, $needReply = false)
+    public function sendCommand(string $command, $message, bool $needReply = false): ?Promise
     {
         if (false == $message instanceof Message) {
             $message = new Message($message);
         }
 
+        $preSend = new PreSend($command, $message, $this, $this->driver);
+        $this->extension->onPreSendCommand($preSend);
+
+        $command = $preSend->getCommand();
+        $message = $preSend->getMessage();
+
         $deleteReplyQueue = false;
         $replyTo = $message->getReplyTo();
 
@@ -117,11 +79,10 @@ public function sendCommand($command, $message, $needReply = false)
             }
         }
 
-        $message->setProperty(Config::PARAMETER_TOPIC_NAME, Config::COMMAND_TOPIC);
-        $message->setProperty(Config::PARAMETER_COMMAND_NAME, $command);
+        $message->setProperty(Config::COMMAND, $command);
         $message->setScope(Message::SCOPE_APP);
 
-        $this->sendEvent(Config::COMMAND_TOPIC, $message);
+        $this->doSend($message);
 
         if ($needReply) {
             $promise = $this->rpcFactory->createPromise($replyTo, $message->getCorrelationId(), 60000);
@@ -129,59 +90,38 @@ public function sendCommand($command, $message, $needReply = false)
 
             return $promise;
         }
-    }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function send($topic, $message)
-    {
-        $this->sendEvent($topic, $message);
+        return null;
     }
 
-    /**
-     * @param Message $message
-     */
-    private function prepareBody(Message $message)
+    private function doSend(Message $message): void
     {
-        $body = $message->getBody();
-        $contentType = $message->getContentType();
-
-        if (is_scalar($body) || null === $body) {
-            $contentType = $contentType ?: 'text/plain';
-            $body = (string) $body;
-        } elseif (is_array($body)) {
-            if ($contentType && $contentType !== 'application/json') {
-                throw new \LogicException(sprintf('Content type "application/json" only allowed when body is array'));
-            }
+        if (false === is_string($message->getBody())) {
+            throw new \LogicException(sprintf('The message body must be string at this stage, got "%s". Make sure you passed string as message or there is an extension that converts custom input to string.', is_object($message->getBody()) ? get_class($message->getBody()) : gettype($message->getBody())));
+        }
 
-            // only array of scalars is allowed.
-            array_walk_recursive($body, function ($value) {
-                if (!is_scalar($value) && null !== $value) {
-                    throw new \LogicException(sprintf(
-                        'The message\'s body must be an array of scalars. Found not scalar in the array: %s',
-                        is_object($value) ? get_class($value) : gettype($value)
-                    ));
-                }
-            });
-
-            $contentType = 'application/json';
-            $body = JSON::encode($body);
-        } elseif ($body instanceof \JsonSerializable) {
-            if ($contentType && $contentType !== 'application/json') {
-                throw new \LogicException(sprintf('Content type "application/json" only allowed when body is array'));
-            }
+        if ($message->getProperty(Config::PROCESSOR)) {
+            throw new \LogicException(sprintf('The %s property must not be set.', Config::PROCESSOR));
+        }
 
-            $contentType = 'application/json';
-            $body = JSON::encode($body);
+        if (!$message->getMessageId()) {
+            $message->setMessageId(UUID::generate());
+        }
+
+        if (!$message->getTimestamp()) {
+            $message->setTimestamp(time());
+        }
+
+        $this->extension->onDriverPreSend(new DriverPreSend($message, $this, $this->driver));
+
+        if (Message::SCOPE_MESSAGE_BUS == $message->getScope()) {
+            $result = $this->driver->sendToRouter($message);
+        } elseif (Message::SCOPE_APP == $message->getScope()) {
+            $result = $this->driver->sendToProcessor($message);
         } else {
-            throw new \InvalidArgumentException(sprintf(
-                'The message\'s body must be either null, scalar, array or object (implements \JsonSerializable). Got: %s',
-                is_object($body) ? get_class($body) : gettype($body)
-            ));
+            throw new \LogicException(sprintf('The message scope "%s" is not supported.', $message->getScope()));
         }
 
-        $message->setContentType($contentType);
-        $message->setBody($body);
+        $this->extension->onPostSend(new PostSend($message, $this, $this->driver, $result->getTransportDestination(), $result->getTransportMessage()));
     }
 }
diff --git a/Client/ProducerInterface.php b/Client/ProducerInterface.php
index 2fc829e..3c88480 100644
--- a/Client/ProducerInterface.php
+++ b/Client/ProducerInterface.php
@@ -7,17 +7,21 @@
 interface ProducerInterface
 {
     /**
-     * @param string               $topic
+     * The message could be pretty much everything as long as you have a client extension that transforms a body to string on onPreSendEvent.
+     *
      * @param string|array|Message $message
+     *
+     * @throws \Exception
      */
-    public function sendEvent($topic, $message);
+    public function sendEvent(string $topic, $message): void;
 
     /**
-     * @param string               $command
+     * The message could be pretty much everything as long as you have a client extension that transforms a body to string on onPreSendCommand.
+     * The promise is returned if needReply argument is true.
+     *
      * @param string|array|Message $message
-     * @param bool                 $needReply
      *
-     * @return Promise|null the promise is returned if needReply argument is true
+     * @throws \Exception
      */
-    public function sendCommand($command, $message, $needReply = false);
+    public function sendCommand(string $command, $message, bool $needReply = false): ?Promise;
 }
diff --git a/Client/Resources.php b/Client/Resources.php
new file mode 100644
index 0000000..a5cc684
--- /dev/null
+++ b/Client/Resources.php
@@ -0,0 +1,194 @@
+<?php
+
+namespace Enqueue\Client;
+
+use Enqueue\Client\Driver\AmqpDriver;
+use Enqueue\Client\Driver\DbalDriver;
+use Enqueue\Client\Driver\FsDriver;
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\Driver\GpsDriver;
+use Enqueue\Client\Driver\MongodbDriver;
+use Enqueue\Client\Driver\RabbitMqDriver;
+use Enqueue\Client\Driver\RabbitMqStompDriver;
+use Enqueue\Client\Driver\RdKafkaDriver;
+use Enqueue\Client\Driver\RedisDriver;
+use Enqueue\Client\Driver\SnsQsDriver;
+use Enqueue\Client\Driver\SqsDriver;
+use Enqueue\Client\Driver\StompDriver;
+
+final class Resources
+{
+    /**
+     * [client driver class => [
+     *   schemes => [schemes strings],
+     *   package => package name,
+     * ].
+     *
+     * @var array
+     */
+    private static $knownDrivers;
+
+    private function __construct()
+    {
+    }
+
+    public static function getAvailableDrivers(): array
+    {
+        $map = self::getKnownDrivers();
+
+        $availableMap = [];
+        foreach ($map as $item) {
+            if (class_exists($item['driverClass'])) {
+                $availableMap[] = $item;
+            }
+        }
+
+        return $availableMap;
+    }
+
+    public static function getKnownDrivers(): array
+    {
+        if (null === self::$knownDrivers) {
+            $map = [];
+
+            $map[] = [
+                'schemes' => ['amqp', 'amqps'],
+                'driverClass' => AmqpDriver::class,
+                'requiredSchemeExtensions' => [],
+                'packages' => ['enqueue/enqueue', 'enqueue/amqp-bunny'],
+            ];
+            $map[] = [
+                'schemes' => ['amqp', 'amqps'],
+                'driverClass' => RabbitMqDriver::class,
+                'requiredSchemeExtensions' => ['rabbitmq'],
+                'packages' => ['enqueue/enqueue', 'enqueue/amqp-bunny'],
+            ];
+            $map[] = [
+                'schemes' => ['file'],
+                'driverClass' => FsDriver::class,
+                'requiredSchemeExtensions' => [],
+                'packages' => ['enqueue/enqueue', 'enqueue/fs'],
+            ];
+            $map[] = [
+                'schemes' => ['null'],
+                'driverClass' => GenericDriver::class,
+                'requiredSchemeExtensions' => [],
+                'packages' => ['enqueue/enqueue', 'enqueue/null'],
+            ];
+            $map[] = [
+                'schemes' => ['gps'],
+                'driverClass' => GpsDriver::class,
+                'requiredSchemeExtensions' => [],
+                'packages' => ['enqueue/enqueue', 'enqueue/gps'],
+            ];
+            $map[] = [
+                'schemes' => ['redis', 'rediss'],
+                'driverClass' => RedisDriver::class,
+                'requiredSchemeExtensions' => [],
+                'packages' => ['enqueue/enqueue', 'enqueue/redis'],
+            ];
+            $map[] = [
+                'schemes' => ['sqs'],
+                'driverClass' => SqsDriver::class,
+                'requiredSchemeExtensions' => [],
+                'packages' => ['enqueue/enqueue', 'enqueue/sqs'],
+            ];
+            $map[] = [
+                'schemes' => ['sns'],
+                'driverClass' => GenericDriver::class,
+                'requiredSchemeExtensions' => [],
+                'packages' => ['enqueue/enqueue', 'enqueue/sns'],
+            ];
+            $map[] = [
+                'schemes' => ['snsqs'],
+                'driverClass' => SnsQsDriver::class,
+                'requiredSchemeExtensions' => [],
+                'packages' => ['enqueue/enqueue', 'enqueue/sqs', 'enqueue/sns', 'enqueue/snsqs'],
+            ];
+            $map[] = [
+                'schemes' => ['stomp'],
+                'driverClass' => StompDriver::class,
+                'requiredSchemeExtensions' => [],
+                'packages' => ['enqueue/enqueue', 'enqueue/stomp'],
+            ];
+            $map[] = [
+                'schemes' => ['stomp'],
+                'driverClass' => RabbitMqStompDriver::class,
+                'requiredSchemeExtensions' => ['rabbitmq'],
+                'packages' => ['enqueue/enqueue', 'enqueue/stomp'],
+            ];
+            $map[] = [
+                'schemes' => ['kafka', 'rdkafka'],
+                'driverClass' => RdKafkaDriver::class,
+                'requiredSchemeExtensions' => [],
+                'packages' => ['enqueue/enqueue', 'enqueue/rdkafka'],
+            ];
+            $map[] = [
+                'schemes' => ['mongodb'],
+                'driverClass' => MongodbDriver::class,
+                'requiredSchemeExtensions' => [],
+                'packages' => ['enqueue/enqueue', 'enqueue/mongodb'],
+            ];
+            $map[] = [
+                'schemes' => [
+                    'db2',
+                    'ibm-db2',
+                    'mssql',
+                    'sqlsrv',
+                    'mysql',
+                    'mysql2',
+                    'mysql',
+                    'pgsql',
+                    'postgres',
+                    'pgsql',
+                    'sqlite',
+                    'sqlite3',
+                    'sqlite',
+                ],
+                'driverClass' => DbalDriver::class,
+                'requiredSchemeExtensions' => [],
+                'package' => ['enqueue/enqueue', 'enqueue/dbal'],
+            ];
+            $map[] = [
+                'schemes' => ['gearman'],
+                'driverClass' => GenericDriver::class,
+                'requiredSchemeExtensions' => [],
+                'package' => ['enqueue/enqueue', 'enqueue/gearman'],
+            ];
+            $map[] = [
+                'schemes' => ['beanstalk'],
+                'driverClass' => GenericDriver::class,
+                'requiredSchemeExtensions' => [],
+                'package' => ['enqueue/enqueue', 'enqueue/pheanstalk'],
+            ];
+
+            self::$knownDrivers = $map;
+        }
+
+        return self::$knownDrivers;
+    }
+
+    public static function addDriver(string $driverClass, array $schemes, array $requiredExtensions, array $packages): void
+    {
+        if (class_exists($driverClass)) {
+            if (false == (new \ReflectionClass($driverClass))->implementsInterface(DriverInterface::class)) {
+                throw new \InvalidArgumentException(sprintf('The driver class "%s" must implement "%s" interface.', $driverClass, DriverInterface::class));
+            }
+        }
+
+        if (empty($schemes)) {
+            throw new \InvalidArgumentException('Schemes could not be empty.');
+        }
+        if (empty($packages)) {
+            throw new \InvalidArgumentException('Packages could not be empty.');
+        }
+
+        self::getKnownDrivers();
+        self::$knownDrivers[] = [
+            'schemes' => $schemes,
+            'driverClass' => $driverClass,
+            'requiredSchemeExtensions' => $requiredExtensions,
+            'packages' => $packages,
+        ];
+    }
+}
diff --git a/Client/Route.php b/Client/Route.php
new file mode 100644
index 0000000..8b9e31e
--- /dev/null
+++ b/Client/Route.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Enqueue\Client;
+
+final class Route
+{
+    public const TOPIC = 'enqueue.client.topic_route';
+
+    public const COMMAND = 'enqueue.client.command_route';
+
+    /**
+     * @var string
+     */
+    private $source;
+
+    /**
+     * @var string
+     */
+    private $sourceType;
+
+    /**
+     * @var string
+     */
+    private $processor;
+
+    /**
+     * @var array
+     */
+    private $options;
+
+    public function __construct(
+        string $source,
+        string $sourceType,
+        string $processor,
+        array $options = [],
+    ) {
+        $this->source = $source;
+        $this->sourceType = $sourceType;
+        $this->processor = $processor;
+        $this->options = $options;
+    }
+
+    public function getSource(): string
+    {
+        return $this->source;
+    }
+
+    public function isCommand(): bool
+    {
+        return self::COMMAND === $this->sourceType;
+    }
+
+    public function isTopic(): bool
+    {
+        return self::TOPIC === $this->sourceType;
+    }
+
+    public function getProcessor(): string
+    {
+        return $this->processor;
+    }
+
+    public function isProcessorExclusive(): bool
+    {
+        return (bool) $this->getOption('exclusive', false);
+    }
+
+    public function isProcessorExternal(): bool
+    {
+        return (bool) $this->getOption('external', false);
+    }
+
+    public function getQueue(): ?string
+    {
+        return $this->getOption('queue');
+    }
+
+    public function isPrefixQueue(): bool
+    {
+        return (bool) $this->getOption('prefix_queue', true);
+    }
+
+    public function getOptions(): array
+    {
+        return $this->options;
+    }
+
+    public function getOption(string $name, $default = null)
+    {
+        return array_key_exists($name, $this->options) ? $this->options[$name] : $default;
+    }
+
+    public function toArray(): array
+    {
+        return array_replace($this->options, [
+            'source' => $this->source,
+            'source_type' => $this->sourceType,
+            'processor' => $this->processor,
+        ]);
+    }
+
+    public static function fromArray(array $route): self
+    {
+        list(
+            'source' => $source,
+            'source_type' => $sourceType,
+            'processor' => $processor) = $route;
+
+        unset($route['source'], $route['source_type'], $route['processor']);
+        $options = $route;
+
+        return new self($source, $sourceType, $processor, $options);
+    }
+}
diff --git a/Client/RouteCollection.php b/Client/RouteCollection.php
new file mode 100644
index 0000000..76bcbe4
--- /dev/null
+++ b/Client/RouteCollection.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Enqueue\Client;
+
+class RouteCollection
+{
+    /**
+     * @var Route[]
+     */
+    private $routes;
+
+    /**
+     * @var Route[]
+     */
+    private $commandRoutes;
+
+    /**
+     * @var Route[]
+     */
+    private $topicRoutes;
+
+    /**
+     * @param Route[] $routes
+     */
+    public function __construct(array $routes)
+    {
+        $this->routes = $routes;
+    }
+
+    public function add(Route $route): void
+    {
+        $this->routes[] = $route;
+        $this->topicRoutes = null;
+        $this->commandRoutes = null;
+    }
+
+    /**
+     * @return Route[]
+     */
+    public function all(): array
+    {
+        return $this->routes;
+    }
+
+    /**
+     * @return Route[]
+     */
+    public function command(string $command): ?Route
+    {
+        if (null === $this->commandRoutes) {
+            $commandRoutes = [];
+            foreach ($this->routes as $route) {
+                if ($route->isCommand()) {
+                    $commandRoutes[$route->getSource()] = $route;
+                }
+            }
+
+            $this->commandRoutes = $commandRoutes;
+        }
+
+        return array_key_exists($command, $this->commandRoutes) ? $this->commandRoutes[$command] : null;
+    }
+
+    /**
+     * @return Route[]
+     */
+    public function topic(string $topic): array
+    {
+        if (null === $this->topicRoutes) {
+            $topicRoutes = [];
+            foreach ($this->routes as $route) {
+                if ($route->isTopic()) {
+                    $topicRoutes[$route->getSource()][$route->getProcessor()] = $route;
+                }
+            }
+
+            $this->topicRoutes = $topicRoutes;
+        }
+
+        return array_key_exists($topic, $this->topicRoutes) ? $this->topicRoutes[$topic] : [];
+    }
+
+    public function topicAndProcessor(string $topic, string $processor): ?Route
+    {
+        $routes = $this->topic($topic);
+        foreach ($routes as $route) {
+            if ($route->getProcessor() === $processor) {
+                return $route;
+            }
+        }
+
+        return null;
+    }
+
+    public function toArray(): array
+    {
+        $rawRoutes = [];
+        foreach ($this->routes as $route) {
+            $rawRoutes[] = $route->toArray();
+        }
+
+        return $rawRoutes;
+    }
+
+    public static function fromArray(array $rawRoutes): self
+    {
+        $routes = [];
+        foreach ($rawRoutes as $rawRoute) {
+            $routes[] = Route::fromArray($rawRoute);
+        }
+
+        return new self($routes);
+    }
+}
diff --git a/Client/RouterProcessor.php b/Client/RouterProcessor.php
index 35efe1b..c441ceb 100644
--- a/Client/RouterProcessor.php
+++ b/Client/RouterProcessor.php
@@ -3,119 +3,62 @@
 namespace Enqueue\Client;
 
 use Enqueue\Consumption\Result;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrMessage;
-use Interop\Queue\PsrProcessor;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Processor;
 
-class RouterProcessor implements PsrProcessor
+final class RouterProcessor implements Processor
 {
     /**
-     * @var DriverInterface
-     */
-    private $driver;
-
-    /**
-     * @var array
+     * compatibility with 0.8x.
      */
-    private $eventRoutes;
+    private const COMMAND_TOPIC_08X = '__command__';
 
     /**
-     * @var array
+     * @var DriverInterface
      */
-    private $commandRoutes;
+    private $driver;
 
-    /**
-     * @param DriverInterface $driver
-     * @param array           $eventRoutes
-     * @param array           $commandRoutes
-     */
-    public function __construct(DriverInterface $driver, array $eventRoutes = [], array $commandRoutes = [])
+    public function __construct(DriverInterface $driver)
     {
         $this->driver = $driver;
-
-        $this->eventRoutes = $eventRoutes;
-        $this->commandRoutes = $commandRoutes;
     }
 
-    /**
-     * @param string $topicName
-     * @param string $queueName
-     * @param string $processorName
-     */
-    public function add($topicName, $queueName, $processorName)
+    public function process(InteropMessage $message, Context $context): Result
     {
-        if (Config::COMMAND_TOPIC === $topicName) {
-            $this->commandRoutes[$processorName] = $queueName;
-        } else {
-            $this->eventRoutes[$topicName][] = [$processorName, $queueName];
+        // compatibility with 0.8x
+        if (self::COMMAND_TOPIC_08X === $message->getProperty(Config::TOPIC)) {
+            $clientMessage = $this->driver->createClientMessage($message);
+            $clientMessage->setProperty(Config::TOPIC, null);
+
+            $this->driver->sendToProcessor($clientMessage);
+
+            return Result::ack('Legacy 0.8x message routed to processor');
         }
-    }
+        // compatibility with 0.8x
 
-    /**
-     * {@inheritdoc}
-     */
-    public function process(PsrMessage $message, PsrContext $context)
-    {
-        $topicName = $message->getProperty(Config::PARAMETER_TOPIC_NAME);
-        if (false == $topicName) {
+        if ($message->getProperty(Config::COMMAND)) {
             return Result::reject(sprintf(
-                'Got message without required parameter: "%s"',
-                Config::PARAMETER_TOPIC_NAME
+                'Unexpected command "%s" got. Command must not go to the router.',
+                $message->getProperty(Config::COMMAND)
             ));
         }
 
-        if (Config::COMMAND_TOPIC === $topicName) {
-            return $this->routeCommand($message);
+        $topic = $message->getProperty(Config::TOPIC);
+        if (false == $topic) {
+            return Result::reject(sprintf('Topic property "%s" is required but not set or empty.', Config::TOPIC));
         }
 
-        return $this->routeEvent($message);
-    }
-
-    /**
-     * @param PsrMessage $message
-     *
-     * @return string|Result
-     */
-    private function routeEvent(PsrMessage $message)
-    {
-        $topicName = $message->getProperty(Config::PARAMETER_TOPIC_NAME);
-
-        if (array_key_exists($topicName, $this->eventRoutes)) {
-            foreach ($this->eventRoutes[$topicName] as $route) {
-                $processorMessage = clone $message;
-                $processorMessage->setProperty(Config::PARAMETER_PROCESSOR_NAME, $route[0]);
-                $processorMessage->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $route[1]);
-
-                $this->driver->sendToProcessor($this->driver->createClientMessage($processorMessage));
-            }
-        }
-
-        return self::ACK;
-    }
-
-    /**
-     * @param PsrMessage $message
-     *
-     * @return string|Result
-     */
-    private function routeCommand(PsrMessage $message)
-    {
-        $commandName = $message->getProperty(Config::PARAMETER_COMMAND_NAME);
-        if (false == $commandName) {
-            return Result::reject(sprintf(
-                'Got message without required parameter: "%s"',
-                Config::PARAMETER_COMMAND_NAME
-            ));
-        }
+        $count = 0;
+        foreach ($this->driver->getRouteCollection()->topic($topic) as $route) {
+            $clientMessage = $this->driver->createClientMessage($message);
+            $clientMessage->setProperty(Config::PROCESSOR, $route->getProcessor());
 
-        if (isset($this->commandRoutes[$commandName])) {
-            $processorMessage = clone $message;
-            $processorMessage->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $this->commandRoutes[$commandName]);
-            $processorMessage->setProperty(Config::PARAMETER_PROCESSOR_NAME, $commandName);
+            $this->driver->sendToProcessor($clientMessage);
 
-            $this->driver->sendToProcessor($this->driver->createClientMessage($processorMessage));
+            ++$count;
         }
 
-        return self::ACK;
+        return Result::ack(sprintf('Routed to "%d" event subscribers', $count));
     }
 }
diff --git a/Client/SpoolProducer.php b/Client/SpoolProducer.php
index c538877..8ad0940 100644
--- a/Client/SpoolProducer.php
+++ b/Client/SpoolProducer.php
@@ -2,6 +2,8 @@
 
 namespace Enqueue\Client;
 
+use Enqueue\Rpc\Promise;
+
 class SpoolProducer implements ProducerInterface
 {
     /**
@@ -19,9 +21,6 @@ class SpoolProducer implements ProducerInterface
      */
     private $commands;
 
-    /**
-     * @param ProducerInterface $realProducer
-     */
     public function __construct(ProducerInterface $realProducer)
     {
         $this->realProducer = $realProducer;
@@ -30,38 +29,26 @@ public function __construct(ProducerInterface $realProducer)
         $this->commands = new \SplQueue();
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function sendCommand($command, $message, $needReply = false)
+    public function sendCommand(string $command, $message, bool $needReply = false): ?Promise
     {
         if ($needReply) {
             return $this->realProducer->sendCommand($command, $message, $needReply);
         }
 
         $this->commands->enqueue([$command, $message]);
-    }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function sendEvent($topic, $message)
-    {
-        $this->events->enqueue([$topic, $message]);
+        return null;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function send($topic, $message)
+    public function sendEvent(string $topic, $message): void
     {
-        $this->sendEvent($topic, $message);
+        $this->events->enqueue([$topic, $message]);
     }
 
     /**
      * When it is called it sends all previously queued messages.
      */
-    public function flush()
+    public function flush(): void
     {
         while (false == $this->events->isEmpty()) {
             list($topic, $message) = $this->events->dequeue();
diff --git a/Client/TopicSubscriberInterface.php b/Client/TopicSubscriberInterface.php
index bdaa43f..849a782 100644
--- a/Client/TopicSubscriberInterface.php
+++ b/Client/TopicSubscriberInterface.php
@@ -7,21 +7,35 @@ interface TopicSubscriberInterface
     /**
      * The result maybe either:.
      *
-     * ['aTopicName']
+     * 'aTopicName'
      *
      * or
      *
-     * ['aTopicName' => [
-     *     'processorName' => 'processor',
-     *     'queueName' => 'a_client_queue_name',
-     *     'queueNameHardcoded' => true,
-     *   ]]
+     * ['aTopicName', 'anotherTopicName']
      *
-     * processorName, queueName and queueNameHardcoded are optional.
+     * or
+     *
+     * [
+     *   [
+     *     'topic' => 'aTopicName',
+     *     'processor' => 'fooProcessor',
+     *     'queue' => 'a_client_queue_name',
+     *
+     *     'aCustomOption' => 'aVal',
+     *   ],
+     *   [
+     *     'topic' => 'anotherTopicName',
+     *     'processor' => 'barProcessor',
+     *     'queue' => 'a_client_queue_name',
+     *
+     *     'aCustomOption' => 'aVal',
+     *   ],
+     * ]
      *
-     * Note: If you set queueNameHardcoded to true then the queueName is used as is and therefor the driver is not used to create a transport queue name.
+     * Note: If you set prefix_queue to true then the queue is used as is and therefor the driver is not used to prepare a transport queue name.
+     * It is possible to pass other options, they could be accessible on a route instance through options.
      *
-     * @return array
+     * @return string|array
      */
     public static function getSubscribedTopics();
 }
diff --git a/Client/TraceableProducer.php b/Client/TraceableProducer.php
index cd55a91..b0bd613 100644
--- a/Client/TraceableProducer.php
+++ b/Client/TraceableProducer.php
@@ -2,61 +2,42 @@
 
 namespace Enqueue\Client;
 
-class TraceableProducer implements ProducerInterface
+use Enqueue\Rpc\Promise;
+
+final class TraceableProducer implements ProducerInterface
 {
     /**
      * @var array
      */
-    protected $traces = [];
+    private $traces = [];
+
     /**
      * @var ProducerInterface
      */
     private $producer;
 
-    /**
-     * @param ProducerInterface $producer
-     */
     public function __construct(ProducerInterface $producer)
     {
         $this->producer = $producer;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function sendEvent($topic, $message)
+    public function sendEvent(string $topic, $message): void
     {
         $this->producer->sendEvent($topic, $message);
 
         $this->collectTrace($topic, null, $message);
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function sendCommand($command, $message, $needReply = false)
+    public function sendCommand(string $command, $message, bool $needReply = false): ?Promise
     {
         $result = $this->producer->sendCommand($command, $message, $needReply);
 
-        $this->collectTrace(Config::COMMAND_TOPIC, $command, $message);
+        $this->collectTrace(null, $command, $message);
 
         return $result;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function send($topic, $message)
-    {
-        $this->sendEvent($topic, $message);
-    }
-
-    /**
-     * @param string $topic
-     *
-     * @return array
-     */
-    public function getTopicTraces($topic)
+    public function getTopicTraces(string $topic): array
     {
         $topicTraces = [];
         foreach ($this->traces as $trace) {
@@ -68,12 +49,7 @@ public function getTopicTraces($topic)
         return $topicTraces;
     }
 
-    /**
-     * @param string $command
-     *
-     * @return array
-     */
-    public function getCommandTraces($command)
+    public function getCommandTraces(string $command): array
     {
         $commandTraces = [];
         foreach ($this->traces as $trace) {
@@ -85,25 +61,17 @@ public function getCommandTraces($command)
         return $commandTraces;
     }
 
-    /**
-     * @return array
-     */
-    public function getTraces()
+    public function getTraces(): array
     {
         return $this->traces;
     }
 
-    public function clearTraces()
+    public function clearTraces(): void
     {
         $this->traces = [];
     }
 
-    /**
-     * @param string|null $topic
-     * @param string|null $command
-     * @param mixed       $message
-     */
-    private function collectTrace($topic, $command, $message)
+    private function collectTrace(?string $topic, ?string $command, $message): void
     {
         $trace = [
             'topic' => $topic,
@@ -117,7 +85,9 @@ private function collectTrace($topic, $command, $message)
             'timestamp' => null,
             'contentType' => null,
             'messageId' => null,
+            'sentAt' => (new \DateTime())->format('Y-m-d H:i:s.u'),
         ];
+
         if ($message instanceof Message) {
             $trace['body'] = $message->getBody();
             $trace['headers'] = $message->getHeaders();
diff --git a/ConnectionFactoryFactory.php b/ConnectionFactoryFactory.php
new file mode 100644
index 0000000..d23518c
--- /dev/null
+++ b/ConnectionFactoryFactory.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Enqueue;
+
+use Enqueue\Dsn\Dsn;
+use Interop\Queue\ConnectionFactory;
+
+final class ConnectionFactoryFactory implements ConnectionFactoryFactoryInterface
+{
+    public function create($config): ConnectionFactory
+    {
+        if (is_string($config)) {
+            $config = ['dsn' => $config];
+        }
+
+        if (false == is_array($config)) {
+            throw new \InvalidArgumentException('The config must be either array or DSN string.');
+        }
+
+        if (false == array_key_exists('dsn', $config)) {
+            throw new \InvalidArgumentException('The config must have dsn key set.');
+        }
+
+        $dsn = Dsn::parseFirst($config['dsn']);
+
+        if ($factoryClass = $this->findFactoryClass($dsn, Resources::getAvailableConnections())) {
+            return new $factoryClass(1 === count($config) ? $config['dsn'] : $config);
+        }
+
+        $knownConnections = Resources::getKnownConnections();
+        if ($factoryClass = $this->findFactoryClass($dsn, $knownConnections)) {
+            throw new \LogicException(sprintf('To use given scheme "%s" a package has to be installed. Run "composer req %s" to add it.', $dsn->getScheme(), $knownConnections[$factoryClass]['package']));
+        }
+
+        throw new \LogicException(sprintf('A given scheme "%s" is not supported. Maybe it is a custom connection, make sure you registered it with "%s::addConnection".', $dsn->getScheme(), Resources::class));
+    }
+
+    private function findFactoryClass(Dsn $dsn, array $factories): ?string
+    {
+        $protocol = $dsn->getSchemeProtocol();
+
+        if ($dsn->getSchemeExtensions()) {
+            foreach ($factories as $connectionClass => $info) {
+                if (empty($info['supportedSchemeExtensions'])) {
+                    continue;
+                }
+
+                if (false == in_array($protocol, $info['schemes'], true)) {
+                    continue;
+                }
+
+                $diff = array_diff($info['supportedSchemeExtensions'], $dsn->getSchemeExtensions());
+                if (empty($diff)) {
+                    return $connectionClass;
+                }
+            }
+        }
+
+        foreach ($factories as $driverClass => $info) {
+            if (false == in_array($protocol, $info['schemes'], true)) {
+                continue;
+            }
+
+            return $driverClass;
+        }
+
+        return null;
+    }
+}
diff --git a/ConnectionFactoryFactoryInterface.php b/ConnectionFactoryFactoryInterface.php
new file mode 100644
index 0000000..f4ca4a6
--- /dev/null
+++ b/ConnectionFactoryFactoryInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Enqueue;
+
+use Interop\Queue\ConnectionFactory;
+
+interface ConnectionFactoryFactoryInterface
+{
+    /**
+     * If string is used, it should be a valid DSN.
+     *
+     * If array is used, it must have a dsn key with valid DSN string.
+     * The other array options are treated as default values.
+     * Options from DSN overwrite them.
+     *
+     * @param string|array $config
+     *
+     * @throws \InvalidArgumentException if invalid config provided
+     */
+    public function create($config): ConnectionFactory;
+}
diff --git a/Consumption/BoundProcessor.php b/Consumption/BoundProcessor.php
new file mode 100644
index 0000000..54fa3f1
--- /dev/null
+++ b/Consumption/BoundProcessor.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Interop\Queue\Processor;
+use Interop\Queue\Queue;
+
+final class BoundProcessor
+{
+    /**
+     * @var Queue
+     */
+    private $queue;
+
+    /**
+     * @var Processor
+     */
+    private $processor;
+
+    public function __construct(Queue $queue, Processor $processor)
+    {
+        $this->queue = $queue;
+        $this->processor = $processor;
+    }
+
+    public function getQueue(): Queue
+    {
+        return $this->queue;
+    }
+
+    public function getProcessor(): Processor
+    {
+        return $this->processor;
+    }
+}
diff --git a/Consumption/CallbackProcessor.php b/Consumption/CallbackProcessor.php
index b002235..d15978f 100644
--- a/Consumption/CallbackProcessor.php
+++ b/Consumption/CallbackProcessor.php
@@ -2,29 +2,23 @@
 
 namespace Enqueue\Consumption;
 
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrMessage;
-use Interop\Queue\PsrProcessor;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Processor;
 
-class CallbackProcessor implements PsrProcessor
+class CallbackProcessor implements Processor
 {
     /**
      * @var callable
      */
     private $callback;
 
-    /**
-     * @param callable $callback
-     */
     public function __construct(callable $callback)
     {
         $this->callback = $callback;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function process(PsrMessage $message, PsrContext $context)
+    public function process(InteropMessage $message, Context $context)
     {
         return call_user_func($this->callback, $message, $context);
     }
diff --git a/Consumption/ChainExtension.php b/Consumption/ChainExtension.php
index d7a24ad..83b4eba 100644
--- a/Consumption/ChainExtension.php
+++ b/Consumption/ChainExtension.php
@@ -2,90 +2,193 @@
 
 namespace Enqueue\Consumption;
 
+use Enqueue\Consumption\Context\End;
+use Enqueue\Consumption\Context\InitLogger;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\Context\MessageResult;
+use Enqueue\Consumption\Context\PostConsume;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\PreConsume;
+use Enqueue\Consumption\Context\PreSubscribe;
+use Enqueue\Consumption\Context\ProcessorException;
+use Enqueue\Consumption\Context\Start;
+
 class ChainExtension implements ExtensionInterface
 {
-    use EmptyExtensionTrait;
-
-    /**
-     * @var ExtensionInterface[]
-     */
-    private $extensions;
+    private $startExtensions;
+    private $initLoggerExtensions;
+    private $preSubscribeExtensions;
+    private $preConsumeExtensions;
+    private $messageReceivedExtensions;
+    private $messageResultExtensions;
+    private $postMessageReceivedExtensions;
+    private $processorExceptionExtensions;
+    private $postConsumeExtensions;
+    private $endExtensions;
 
-    /**
-     * @param ExtensionInterface[] $extensions
-     */
     public function __construct(array $extensions)
     {
-        $this->extensions = $extensions;
+        $this->startExtensions = [];
+        $this->initLoggerExtensions = [];
+        $this->preSubscribeExtensions = [];
+        $this->preConsumeExtensions = [];
+        $this->messageReceivedExtensions = [];
+        $this->messageResultExtensions = [];
+        $this->postMessageReceivedExtensions = [];
+        $this->processorExceptionExtensions = [];
+        $this->postConsumeExtensions = [];
+        $this->endExtensions = [];
+
+        array_walk($extensions, function ($extension) {
+            if ($extension instanceof ExtensionInterface) {
+                $this->startExtensions[] = $extension;
+                $this->initLoggerExtensions[] = $extension;
+                $this->preSubscribeExtensions[] = $extension;
+                $this->preConsumeExtensions[] = $extension;
+                $this->messageReceivedExtensions[] = $extension;
+                $this->messageResultExtensions[] = $extension;
+                $this->postMessageReceivedExtensions[] = $extension;
+                $this->processorExceptionExtensions[] = $extension;
+                $this->postConsumeExtensions[] = $extension;
+                $this->endExtensions[] = $extension;
+
+                return;
+            }
+
+            $extensionValid = false;
+            if ($extension instanceof StartExtensionInterface) {
+                $this->startExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if ($extension instanceof InitLoggerExtensionInterface) {
+                $this->initLoggerExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if ($extension instanceof PreSubscribeExtensionInterface) {
+                $this->preSubscribeExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if ($extension instanceof PreConsumeExtensionInterface) {
+                $this->preConsumeExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if ($extension instanceof MessageReceivedExtensionInterface) {
+                $this->messageReceivedExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if ($extension instanceof MessageResultExtensionInterface) {
+                $this->messageResultExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if ($extension instanceof ProcessorExceptionExtensionInterface) {
+                $this->processorExceptionExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if ($extension instanceof PostMessageReceivedExtensionInterface) {
+                $this->postMessageReceivedExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if ($extension instanceof PostConsumeExtensionInterface) {
+                $this->postConsumeExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if ($extension instanceof EndExtensionInterface) {
+                $this->endExtensions[] = $extension;
+
+                $extensionValid = true;
+            }
+
+            if (false == $extensionValid) {
+                throw new \LogicException(sprintf('Invalid extension given %s', $extension::class));
+            }
+        });
+    }
+
+    public function onInitLogger(InitLogger $context): void
+    {
+        foreach ($this->initLoggerExtensions as $extension) {
+            $extension->onInitLogger($context);
+        }
     }
 
-    /**
-     * @param Context $context
-     */
-    public function onStart(Context $context)
+    public function onStart(Start $context): void
     {
-        foreach ($this->extensions as $extension) {
+        foreach ($this->startExtensions as $extension) {
             $extension->onStart($context);
         }
     }
 
-    /**
-     * @param Context $context
-     */
-    public function onBeforeReceive(Context $context)
+    public function onPreSubscribe(PreSubscribe $context): void
+    {
+        foreach ($this->preSubscribeExtensions as $extension) {
+            $extension->onPreSubscribe($context);
+        }
+    }
+
+    public function onPreConsume(PreConsume $context): void
     {
-        foreach ($this->extensions as $extension) {
-            $extension->onBeforeReceive($context);
+        foreach ($this->preConsumeExtensions as $extension) {
+            $extension->onPreConsume($context);
         }
     }
 
-    /**
-     * @param Context $context
-     */
-    public function onPreReceived(Context $context)
+    public function onMessageReceived(MessageReceived $context): void
     {
-        foreach ($this->extensions as $extension) {
-            $extension->onPreReceived($context);
+        foreach ($this->messageReceivedExtensions as $extension) {
+            $extension->onMessageReceived($context);
         }
     }
 
-    /**
-     * @param Context $context
-     */
-    public function onResult(Context $context)
+    public function onResult(MessageResult $context): void
     {
-        foreach ($this->extensions as $extension) {
+        foreach ($this->messageResultExtensions as $extension) {
             $extension->onResult($context);
         }
     }
 
-    /**
-     * @param Context $context
-     */
-    public function onPostReceived(Context $context)
+    public function onProcessorException(ProcessorException $context): void
+    {
+        foreach ($this->processorExceptionExtensions as $extension) {
+            $extension->onProcessorException($context);
+        }
+    }
+
+    public function onPostMessageReceived(PostMessageReceived $context): void
     {
-        foreach ($this->extensions as $extension) {
-            $extension->onPostReceived($context);
+        foreach ($this->postMessageReceivedExtensions as $extension) {
+            $extension->onPostMessageReceived($context);
         }
     }
 
-    /**
-     * @param Context $context
-     */
-    public function onIdle(Context $context)
+    public function onPostConsume(PostConsume $context): void
     {
-        foreach ($this->extensions as $extension) {
-            $extension->onIdle($context);
+        foreach ($this->postConsumeExtensions as $extension) {
+            $extension->onPostConsume($context);
         }
     }
 
-    /**
-     * @param Context $context
-     */
-    public function onInterrupted(Context $context)
+    public function onEnd(End $context): void
     {
-        foreach ($this->extensions as $extension) {
-            $extension->onInterrupted($context);
+        foreach ($this->endExtensions as $extension) {
+            $extension->onEnd($context);
         }
     }
 }
diff --git a/Consumption/Context.php b/Consumption/Context.php
deleted file mode 100644
index 09332b9..0000000
--- a/Consumption/Context.php
+++ /dev/null
@@ -1,233 +0,0 @@
-<?php
-
-namespace Enqueue\Consumption;
-
-use Enqueue\Consumption\Exception\IllegalContextModificationException;
-use Interop\Queue\PsrConsumer;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrMessage;
-use Interop\Queue\PsrProcessor;
-use Interop\Queue\PsrQueue;
-use Psr\Log\LoggerInterface;
-
-class Context
-{
-    /**
-     * @var PsrContext
-     */
-    private $psrContext;
-
-    /**
-     * @var PsrConsumer
-     */
-    private $psrConsumer;
-
-    /**
-     * @var PsrProcessor
-     */
-    private $psrProcessor;
-
-    /**
-     * @var LoggerInterface
-     */
-    private $logger;
-
-    /**
-     * @var PsrMessage
-     */
-    private $psrMessage;
-
-    /**
-     * @var \Exception
-     */
-    private $exception;
-
-    /**
-     * @var Result|string
-     */
-    private $result;
-
-    /**
-     * @var PsrQueue
-     */
-    private $psrQueue;
-
-    /**
-     * @var bool
-     */
-    private $executionInterrupted;
-
-    /**
-     * @param PsrContext $psrContext
-     */
-    public function __construct(PsrContext $psrContext)
-    {
-        $this->psrContext = $psrContext;
-
-        $this->executionInterrupted = false;
-    }
-
-    /**
-     * @return PsrMessage
-     */
-    public function getPsrMessage()
-    {
-        return $this->psrMessage;
-    }
-
-    /**
-     * @param PsrMessage $psrMessage
-     */
-    public function setPsrMessage(PsrMessage $psrMessage)
-    {
-        if ($this->psrMessage) {
-            throw new IllegalContextModificationException('The message could be set once');
-        }
-
-        $this->psrMessage = $psrMessage;
-    }
-
-    /**
-     * @return PsrContext
-     */
-    public function getPsrContext()
-    {
-        return $this->psrContext;
-    }
-
-    /**
-     * @return PsrConsumer
-     */
-    public function getPsrConsumer()
-    {
-        return $this->psrConsumer;
-    }
-
-    /**
-     * @param PsrConsumer $psrConsumer
-     */
-    public function setPsrConsumer(PsrConsumer $psrConsumer)
-    {
-        if ($this->psrConsumer) {
-            throw new IllegalContextModificationException('The message consumer could be set once');
-        }
-
-        $this->psrConsumer = $psrConsumer;
-    }
-
-    /**
-     * @return PsrProcessor
-     */
-    public function getPsrProcessor()
-    {
-        return $this->psrProcessor;
-    }
-
-    /**
-     * @param PsrProcessor $psrProcessor
-     */
-    public function setPsrProcessor(PsrProcessor $psrProcessor)
-    {
-        if ($this->psrProcessor) {
-            throw new IllegalContextModificationException('The message processor could be set once');
-        }
-
-        $this->psrProcessor = $psrProcessor;
-    }
-
-    /**
-     * @return \Exception
-     */
-    public function getException()
-    {
-        return $this->exception;
-    }
-
-    /**
-     * @param \Exception $exception
-     */
-    public function setException(\Exception $exception)
-    {
-        $this->exception = $exception;
-    }
-
-    /**
-     * @return Result|string
-     */
-    public function getResult()
-    {
-        return $this->result;
-    }
-
-    /**
-     * @param Result|string $result
-     */
-    public function setResult($result)
-    {
-        if ($this->result) {
-            throw new IllegalContextModificationException('The result modification is not allowed');
-        }
-
-        $this->result = $result;
-    }
-
-    /**
-     * @return bool
-     */
-    public function isExecutionInterrupted()
-    {
-        return $this->executionInterrupted;
-    }
-
-    /**
-     * @param bool $executionInterrupted
-     */
-    public function setExecutionInterrupted($executionInterrupted)
-    {
-        if (false == $executionInterrupted && $this->executionInterrupted) {
-            throw new IllegalContextModificationException('The execution once interrupted could not be roll backed');
-        }
-
-        $this->executionInterrupted = $executionInterrupted;
-    }
-
-    /**
-     * @return LoggerInterface
-     */
-    public function getLogger()
-    {
-        return $this->logger;
-    }
-
-    /**
-     * @param LoggerInterface $logger
-     */
-    public function setLogger(LoggerInterface $logger)
-    {
-        if ($this->logger) {
-            throw new IllegalContextModificationException('The logger modification is not allowed');
-        }
-
-        $this->logger = $logger;
-    }
-
-    /**
-     * @return PsrQueue
-     */
-    public function getPsrQueue()
-    {
-        return $this->psrQueue;
-    }
-
-    /**
-     * @param PsrQueue $psrQueue
-     */
-    public function setPsrQueue(PsrQueue $psrQueue)
-    {
-        if ($this->psrQueue) {
-            throw new IllegalContextModificationException('The queue modification is not allowed');
-        }
-
-        $this->psrQueue = $psrQueue;
-    }
-}
diff --git a/Consumption/Context/End.php b/Consumption/Context/End.php
new file mode 100644
index 0000000..07853b3
--- /dev/null
+++ b/Consumption/Context/End.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Enqueue\Consumption\Context;
+
+use Interop\Queue\Context;
+use Psr\Log\LoggerInterface;
+
+final class End
+{
+    /**
+     * @var Context
+     */
+    private $context;
+
+    /**
+     * @var int
+     */
+    private $startTime;
+
+    /**
+     * @var int
+     */
+    private $endTime;
+
+    /**
+     * @var LoggerInterface
+     */
+    private $logger;
+
+    /**
+     * @var int
+     */
+    private $exitStatus;
+
+    public function __construct(
+        Context $context,
+        int $startTime,
+        int $endTime,
+        LoggerInterface $logger,
+        ?int $exitStatus = null,
+    ) {
+        $this->context = $context;
+        $this->logger = $logger;
+        $this->startTime = $startTime;
+        $this->endTime = $endTime;
+        $this->exitStatus = $exitStatus;
+    }
+
+    public function getContext(): Context
+    {
+        return $this->context;
+    }
+
+    public function getLogger(): LoggerInterface
+    {
+        return $this->logger;
+    }
+
+    /**
+     * In milliseconds.
+     */
+    public function getStartTime(): int
+    {
+        return $this->startTime;
+    }
+
+    /**
+     * In milliseconds.
+     */
+    public function getEndTime(): int
+    {
+        return $this->startTime;
+    }
+
+    public function getExitStatus(): ?int
+    {
+        return $this->exitStatus;
+    }
+}
diff --git a/Consumption/Context/InitLogger.php b/Consumption/Context/InitLogger.php
new file mode 100644
index 0000000..c480572
--- /dev/null
+++ b/Consumption/Context/InitLogger.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Enqueue\Consumption\Context;
+
+use Psr\Log\LoggerInterface;
+
+class InitLogger
+{
+    /**
+     * @var LoggerInterface
+     */
+    private $logger;
+
+    public function __construct(LoggerInterface $logger)
+    {
+        $this->logger = $logger;
+    }
+
+    public function getLogger(): LoggerInterface
+    {
+        return $this->logger;
+    }
+
+    public function changeLogger(LoggerInterface $logger): void
+    {
+        $this->logger = $logger;
+    }
+}
diff --git a/Consumption/Context/MessageReceived.php b/Consumption/Context/MessageReceived.php
new file mode 100644
index 0000000..35abf1c
--- /dev/null
+++ b/Consumption/Context/MessageReceived.php
@@ -0,0 +1,109 @@
+<?php
+
+namespace Enqueue\Consumption\Context;
+
+use Enqueue\Consumption\Result;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Message;
+use Interop\Queue\Processor;
+use Psr\Log\LoggerInterface;
+
+final class MessageReceived
+{
+    /**
+     * @var Context
+     */
+    private $context;
+
+    /**
+     * @var Consumer
+     */
+    private $consumer;
+
+    /**
+     * @var Message
+     */
+    private $message;
+
+    /**
+     * @var Processor
+     */
+    private $processor;
+
+    /**
+     * @var LoggerInterface
+     */
+    private $logger;
+
+    /**
+     * @var int
+     */
+    private $receivedAt;
+
+    /**
+     * @var Result|null
+     */
+    private $result;
+
+    public function __construct(
+        Context $context,
+        Consumer $consumer,
+        Message $message,
+        Processor $processor,
+        int $receivedAt,
+        LoggerInterface $logger,
+    ) {
+        $this->context = $context;
+        $this->consumer = $consumer;
+        $this->message = $message;
+        $this->processor = $processor;
+        $this->receivedAt = $receivedAt;
+        $this->logger = $logger;
+    }
+
+    public function getContext(): Context
+    {
+        return $this->context;
+    }
+
+    public function getConsumer(): Consumer
+    {
+        return $this->consumer;
+    }
+
+    public function getMessage(): Message
+    {
+        return $this->message;
+    }
+
+    public function getProcessor(): Processor
+    {
+        return $this->processor;
+    }
+
+    public function changeProcessor(Processor $processor): void
+    {
+        $this->processor = $processor;
+    }
+
+    public function getLogger(): LoggerInterface
+    {
+        return $this->logger;
+    }
+
+    public function getReceivedAt(): int
+    {
+        return $this->receivedAt;
+    }
+
+    public function getResult(): ?Result
+    {
+        return $this->result;
+    }
+
+    public function setResult(Result $result): void
+    {
+        $this->result = $result;
+    }
+}
diff --git a/Consumption/Context/MessageResult.php b/Consumption/Context/MessageResult.php
new file mode 100644
index 0000000..4fa8f7d
--- /dev/null
+++ b/Consumption/Context/MessageResult.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Enqueue\Consumption\Context;
+
+use Enqueue\Consumption\Result;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Message;
+use Psr\Log\LoggerInterface;
+
+final class MessageResult
+{
+    /**
+     * @var Context
+     */
+    private $context;
+
+    /**
+     * @var Consumer
+     */
+    private $consumer;
+
+    /**
+     * @var Message
+     */
+    private $message;
+
+    /**
+     * @var Result|string|object|null
+     */
+    private $result;
+
+    /**
+     * @var int
+     */
+    private $receivedAt;
+
+    /**
+     * @var LoggerInterface
+     */
+    private $logger;
+
+    public function __construct(Context $context, Consumer $consumer, Message $message, $result, int $receivedAt, LoggerInterface $logger)
+    {
+        $this->context = $context;
+        $this->consumer = $consumer;
+        $this->message = $message;
+        $this->logger = $logger;
+        $this->result = $result;
+        $this->receivedAt = $receivedAt;
+    }
+
+    public function getContext(): Context
+    {
+        return $this->context;
+    }
+
+    public function getConsumer(): Consumer
+    {
+        return $this->consumer;
+    }
+
+    public function getMessage(): Message
+    {
+        return $this->message;
+    }
+
+    public function getLogger(): LoggerInterface
+    {
+        return $this->logger;
+    }
+
+    public function getReceivedAt(): int
+    {
+        return $this->receivedAt;
+    }
+
+    /**
+     * @return Result|object|string|null
+     */
+    public function getResult()
+    {
+        return $this->result;
+    }
+
+    /**
+     * @param Result|string|object|null $result
+     */
+    public function changeResult($result): void
+    {
+        $this->result = $result;
+    }
+}
diff --git a/Consumption/Context/PostConsume.php b/Consumption/Context/PostConsume.php
new file mode 100644
index 0000000..a6f1d83
--- /dev/null
+++ b/Consumption/Context/PostConsume.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Enqueue\Consumption\Context;
+
+use Interop\Queue\Context;
+use Interop\Queue\SubscriptionConsumer;
+use Psr\Log\LoggerInterface;
+
+final class PostConsume
+{
+    /**
+     * @var Context
+     */
+    private $context;
+
+    /**
+     * @var SubscriptionConsumer
+     */
+    private $subscriptionConsumer;
+
+    /**
+     * @var int
+     */
+    private $receivedMessagesCount;
+
+    /**
+     * @var int
+     */
+    private $cycle;
+
+    /**
+     * @var int
+     */
+    private $startTime;
+
+    /**
+     * @var LoggerInterface
+     */
+    private $logger;
+
+    /**
+     * @var bool
+     */
+    private $executionInterrupted;
+
+    /**
+     * @var int
+     */
+    private $exitStatus;
+
+    public function __construct(Context $context, SubscriptionConsumer $subscriptionConsumer, int $receivedMessagesCount, int $cycle, int $startTime, LoggerInterface $logger)
+    {
+        $this->context = $context;
+        $this->subscriptionConsumer = $subscriptionConsumer;
+        $this->receivedMessagesCount = $receivedMessagesCount;
+        $this->cycle = $cycle;
+        $this->startTime = $startTime;
+        $this->logger = $logger;
+
+        $this->executionInterrupted = false;
+    }
+
+    public function getContext(): Context
+    {
+        return $this->context;
+    }
+
+    public function getSubscriptionConsumer(): SubscriptionConsumer
+    {
+        return $this->subscriptionConsumer;
+    }
+
+    public function getReceivedMessagesCount(): int
+    {
+        return $this->receivedMessagesCount;
+    }
+
+    public function getCycle(): int
+    {
+        return $this->cycle;
+    }
+
+    public function getStartTime(): int
+    {
+        return $this->startTime;
+    }
+
+    public function getLogger(): LoggerInterface
+    {
+        return $this->logger;
+    }
+
+    public function getExitStatus(): ?int
+    {
+        return $this->exitStatus;
+    }
+
+    public function isExecutionInterrupted(): bool
+    {
+        return $this->executionInterrupted;
+    }
+
+    public function interruptExecution(?int $exitStatus = null): void
+    {
+        $this->exitStatus = $exitStatus;
+        $this->executionInterrupted = true;
+    }
+}
diff --git a/Consumption/Context/PostMessageReceived.php b/Consumption/Context/PostMessageReceived.php
new file mode 100644
index 0000000..23df2c8
--- /dev/null
+++ b/Consumption/Context/PostMessageReceived.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Enqueue\Consumption\Context;
+
+use Enqueue\Consumption\Result;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Message;
+use Psr\Log\LoggerInterface;
+
+final class PostMessageReceived
+{
+    /**
+     * @var Context
+     */
+    private $context;
+
+    /**
+     * @var Consumer
+     */
+    private $consumer;
+
+    /**
+     * @var Message
+     */
+    private $message;
+
+    /**
+     * @var LoggerInterface
+     */
+    private $logger;
+
+    /**
+     * @var int
+     */
+    private $receivedAt;
+
+    /**
+     * @var Result|string|object|null
+     */
+    private $result;
+
+    /**
+     * @var bool
+     */
+    private $executionInterrupted;
+
+    /**
+     * @var int
+     */
+    private $exitStatus;
+
+    public function __construct(
+        Context $context,
+        Consumer $consumer,
+        Message $message,
+        $result,
+        int $receivedAt,
+        LoggerInterface $logger,
+    ) {
+        $this->context = $context;
+        $this->consumer = $consumer;
+        $this->message = $message;
+        $this->result = $result;
+        $this->receivedAt = $receivedAt;
+        $this->logger = $logger;
+
+        $this->executionInterrupted = false;
+    }
+
+    public function getContext(): Context
+    {
+        return $this->context;
+    }
+
+    public function getConsumer(): Consumer
+    {
+        return $this->consumer;
+    }
+
+    public function getMessage(): Message
+    {
+        return $this->message;
+    }
+
+    public function getLogger(): LoggerInterface
+    {
+        return $this->logger;
+    }
+
+    public function getReceivedAt(): int
+    {
+        return $this->receivedAt;
+    }
+
+    /**
+     * @return Result|object|string|null
+     */
+    public function getResult()
+    {
+        return $this->result;
+    }
+
+    public function getExitStatus(): ?int
+    {
+        return $this->exitStatus;
+    }
+
+    public function isExecutionInterrupted(): bool
+    {
+        return $this->executionInterrupted;
+    }
+
+    public function interruptExecution(?int $exitStatus = null): void
+    {
+        $this->exitStatus = $exitStatus;
+        $this->executionInterrupted = true;
+    }
+}
diff --git a/Consumption/Context/PreConsume.php b/Consumption/Context/PreConsume.php
new file mode 100644
index 0000000..77cc7d0
--- /dev/null
+++ b/Consumption/Context/PreConsume.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Enqueue\Consumption\Context;
+
+use Interop\Queue\Context;
+use Interop\Queue\SubscriptionConsumer;
+use Psr\Log\LoggerInterface;
+
+final class PreConsume
+{
+    /**
+     * @var Context
+     */
+    private $context;
+
+    /**
+     * @var SubscriptionConsumer
+     */
+    private $subscriptionConsumer;
+
+    /**
+     * @var LoggerInterface
+     */
+    private $logger;
+
+    /**
+     * @var int
+     */
+    private $cycle;
+
+    /**
+     * @var int
+     */
+    private $receiveTimeout;
+
+    /**
+     * @var int
+     */
+    private $startTime;
+
+    /**
+     * @var bool
+     */
+    private $executionInterrupted;
+
+    /**
+     * @var int
+     */
+    private $exitStatus;
+
+    public function __construct(Context $context, SubscriptionConsumer $subscriptionConsumer, LoggerInterface $logger, int $cycle, int $receiveTimeout, int $startTime)
+    {
+        $this->context = $context;
+        $this->subscriptionConsumer = $subscriptionConsumer;
+        $this->logger = $logger;
+        $this->cycle = $cycle;
+        $this->receiveTimeout = $receiveTimeout;
+        $this->startTime = $startTime;
+
+        $this->executionInterrupted = false;
+    }
+
+    public function getContext(): Context
+    {
+        return $this->context;
+    }
+
+    public function getSubscriptionConsumer(): SubscriptionConsumer
+    {
+        return $this->subscriptionConsumer;
+    }
+
+    public function getLogger(): LoggerInterface
+    {
+        return $this->logger;
+    }
+
+    public function getCycle(): int
+    {
+        return $this->cycle;
+    }
+
+    public function getReceiveTimeout(): int
+    {
+        return $this->receiveTimeout;
+    }
+
+    public function getStartTime(): int
+    {
+        return $this->startTime;
+    }
+
+    public function getExitStatus(): ?int
+    {
+        return $this->exitStatus;
+    }
+
+    public function isExecutionInterrupted(): bool
+    {
+        return $this->executionInterrupted;
+    }
+
+    public function interruptExecution(?int $exitStatus = null): void
+    {
+        $this->exitStatus = $exitStatus;
+        $this->executionInterrupted = true;
+    }
+}
diff --git a/Consumption/Context/PreSubscribe.php b/Consumption/Context/PreSubscribe.php
new file mode 100644
index 0000000..dbc74bb
--- /dev/null
+++ b/Consumption/Context/PreSubscribe.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Enqueue\Consumption\Context;
+
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Processor;
+use Psr\Log\LoggerInterface;
+
+final class PreSubscribe
+{
+    /**
+     * @var Context
+     */
+    private $context;
+
+    /**
+     * @var Processor
+     */
+    private $processor;
+
+    /**
+     * @var Consumer
+     */
+    private $consumer;
+
+    /**
+     * @var LoggerInterface
+     */
+    private $logger;
+
+    public function __construct(Context $context, Processor $processor, Consumer $consumer, LoggerInterface $logger)
+    {
+        $this->context = $context;
+        $this->processor = $processor;
+        $this->consumer = $consumer;
+        $this->logger = $logger;
+    }
+
+    public function getContext(): Context
+    {
+        return $this->context;
+    }
+
+    public function getProcessor(): Processor
+    {
+        return $this->processor;
+    }
+
+    public function getConsumer(): Consumer
+    {
+        return $this->consumer;
+    }
+
+    public function getLogger(): LoggerInterface
+    {
+        return $this->logger;
+    }
+}
diff --git a/Consumption/Context/ProcessorException.php b/Consumption/Context/ProcessorException.php
new file mode 100644
index 0000000..329b13d
--- /dev/null
+++ b/Consumption/Context/ProcessorException.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Enqueue\Consumption\Context;
+
+use Enqueue\Consumption\Result;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Message;
+use Psr\Log\LoggerInterface;
+
+final class ProcessorException
+{
+    /**
+     * @var Context
+     */
+    private $context;
+
+    /**
+     * @var Consumer
+     */
+    private $consumer;
+
+    /**
+     * @var Message
+     */
+    private $message;
+
+    /**
+     * @var \Throwable
+     */
+    private $exception;
+
+    /**
+     * @var Result|string|object|null
+     */
+    private $result;
+
+    /**
+     * @var int
+     */
+    private $receivedAt;
+    /**
+     * @var LoggerInterface
+     */
+    private $logger;
+
+    public function __construct(Context $context, Consumer $consumer, Message $message, \Throwable $exception, int $receivedAt, LoggerInterface $logger)
+    {
+        $this->context = $context;
+        $this->consumer = $consumer;
+        $this->message = $message;
+        $this->exception = $exception;
+        $this->logger = $logger;
+        $this->receivedAt = $receivedAt;
+    }
+
+    public function getContext(): Context
+    {
+        return $this->context;
+    }
+
+    public function getConsumer(): Consumer
+    {
+        return $this->consumer;
+    }
+
+    public function getMessage(): Message
+    {
+        return $this->message;
+    }
+
+    public function getException(): \Throwable
+    {
+        return $this->exception;
+    }
+
+    public function getLogger(): LoggerInterface
+    {
+        return $this->logger;
+    }
+
+    public function getReceivedAt(): int
+    {
+        return $this->receivedAt;
+    }
+
+    public function getResult(): ?Result
+    {
+        return $this->result;
+    }
+
+    public function setResult(Result $result): void
+    {
+        $this->result = $result;
+    }
+}
diff --git a/Consumption/Context/Start.php b/Consumption/Context/Start.php
new file mode 100644
index 0000000..84db29c
--- /dev/null
+++ b/Consumption/Context/Start.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Enqueue\Consumption\Context;
+
+use Enqueue\Consumption\BoundProcessor;
+use Interop\Queue\Context;
+use Psr\Log\LoggerInterface;
+
+final class Start
+{
+    /**
+     * @var Context
+     */
+    private $context;
+
+    /**
+     * @var LoggerInterface
+     */
+    private $logger;
+
+    /**
+     * @var BoundProcessor[]
+     */
+    private $processors;
+
+    /**
+     * @var int
+     */
+    private $receiveTimeout;
+
+    /**
+     * @var int
+     */
+    private $startTime;
+
+    /**
+     * @var bool
+     */
+    private $executionInterrupted;
+
+    /**
+     * @var int
+     */
+    private $exitStatus;
+
+    /**
+     * @param BoundProcessor[] $processors
+     */
+    public function __construct(Context $context, LoggerInterface $logger, array $processors, int $receiveTimeout, int $startTime)
+    {
+        $this->context = $context;
+        $this->logger = $logger;
+        $this->processors = $processors;
+        $this->receiveTimeout = $receiveTimeout;
+        $this->startTime = $startTime;
+
+        $this->executionInterrupted = false;
+    }
+
+    public function getContext(): Context
+    {
+        return $this->context;
+    }
+
+    public function getLogger(): LoggerInterface
+    {
+        return $this->logger;
+    }
+
+    /**
+     * In milliseconds.
+     */
+    public function getReceiveTimeout(): int
+    {
+        return $this->receiveTimeout;
+    }
+
+    /**
+     * In milliseconds.
+     */
+    public function changeReceiveTimeout(int $timeout): void
+    {
+        $this->receiveTimeout = $timeout;
+    }
+
+    /**
+     * In milliseconds.
+     */
+    public function getStartTime(): int
+    {
+        return $this->startTime;
+    }
+
+    /**
+     * @return BoundProcessor[]
+     */
+    public function getBoundProcessors(): array
+    {
+        return $this->processors;
+    }
+
+    /**
+     * @param BoundProcessor[] $processors
+     */
+    public function changeBoundProcessors(array $processors): void
+    {
+        $this->processors = [];
+        array_walk($processors, function (BoundProcessor $processor) {
+            $this->processors[] = $processor;
+        });
+    }
+
+    public function getExitStatus(): ?int
+    {
+        return $this->exitStatus;
+    }
+
+    public function isExecutionInterrupted(): bool
+    {
+        return $this->executionInterrupted;
+    }
+
+    public function interruptExecution(?int $exitStatus = null): void
+    {
+        $this->exitStatus = $exitStatus;
+        $this->executionInterrupted = true;
+    }
+}
diff --git a/Consumption/EmptyExtensionTrait.php b/Consumption/EmptyExtensionTrait.php
deleted file mode 100644
index 0f6b849..0000000
--- a/Consumption/EmptyExtensionTrait.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-
-namespace Enqueue\Consumption;
-
-trait EmptyExtensionTrait
-{
-    /**
-     * @param Context $context
-     */
-    public function onStart(Context $context)
-    {
-    }
-
-    /**
-     * @param Context $context
-     */
-    public function onBeforeReceive(Context $context)
-    {
-    }
-
-    /**
-     * @param Context $context
-     */
-    public function onPreReceived(Context $context)
-    {
-    }
-
-    /**
-     * @param Context $context
-     */
-    public function onResult(Context $context)
-    {
-    }
-
-    /**
-     * @param Context $context
-     */
-    public function onPostReceived(Context $context)
-    {
-    }
-
-    /**
-     * @param Context $context
-     */
-    public function onIdle(Context $context)
-    {
-    }
-
-    /**
-     * @param Context $context
-     */
-    public function onInterrupted(Context $context)
-    {
-    }
-}
diff --git a/Consumption/EndExtensionInterface.php b/Consumption/EndExtensionInterface.php
new file mode 100644
index 0000000..167779d
--- /dev/null
+++ b/Consumption/EndExtensionInterface.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Enqueue\Consumption\Context\End;
+
+interface EndExtensionInterface
+{
+    /**
+     * Executed only once just before QueueConsumer::consume returns.
+     */
+    public function onEnd(End $context): void;
+}
diff --git a/Consumption/Exception/ConsumptionInterruptedException.php b/Consumption/Exception/ConsumptionInterruptedException.php
deleted file mode 100644
index 0bfe0c1..0000000
--- a/Consumption/Exception/ConsumptionInterruptedException.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-namespace Enqueue\Consumption\Exception;
-
-class ConsumptionInterruptedException extends \LogicException implements ExceptionInterface
-{
-}
diff --git a/Consumption/Exception/InvalidArgumentException.php b/Consumption/Exception/InvalidArgumentException.php
index da6015d..89a2f4c 100644
--- a/Consumption/Exception/InvalidArgumentException.php
+++ b/Consumption/Exception/InvalidArgumentException.php
@@ -5,7 +5,6 @@
 class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
 {
     /**
-     * @param mixed  $argument
      * @param string $class
      *
      * @throws static
@@ -13,11 +12,7 @@ class InvalidArgumentException extends \InvalidArgumentException implements Exce
     public static function assertInstanceOf($argument, $class)
     {
         if (false == $argument instanceof $class) {
-            throw new static(sprintf(
-                'The argument must be an instance of %s but got %s.',
-                $class,
-                is_object($argument) ? get_class($argument) : gettype($argument)
-            ));
+            throw new self(sprintf('The argument must be an instance of %s but got %s.', $class, is_object($argument) ? $argument::class : gettype($argument)));
         }
     }
 }
diff --git a/Consumption/Extension/ExitStatusExtension.php b/Consumption/Extension/ExitStatusExtension.php
new file mode 100644
index 0000000..a430712
--- /dev/null
+++ b/Consumption/Extension/ExitStatusExtension.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Enqueue\Consumption\Extension;
+
+use Enqueue\Consumption\Context\End;
+use Enqueue\Consumption\EndExtensionInterface;
+
+class ExitStatusExtension implements EndExtensionInterface
+{
+    /**
+     * @var int
+     */
+    private $exitStatus;
+
+    public function onEnd(End $context): void
+    {
+        $this->exitStatus = $context->getExitStatus();
+    }
+
+    public function getExitStatus(): ?int
+    {
+        return $this->exitStatus;
+    }
+}
diff --git a/Consumption/Extension/LimitConsumedMessagesExtension.php b/Consumption/Extension/LimitConsumedMessagesExtension.php
index ef6ec52..0dc6fec 100644
--- a/Consumption/Extension/LimitConsumedMessagesExtension.php
+++ b/Consumption/Extension/LimitConsumedMessagesExtension.php
@@ -2,14 +2,14 @@
 
 namespace Enqueue\Consumption\Extension;
 
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\EmptyExtensionTrait;
-use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\PreConsume;
+use Enqueue\Consumption\PostMessageReceivedExtensionInterface;
+use Enqueue\Consumption\PreConsumeExtensionInterface;
+use Psr\Log\LoggerInterface;
 
-class LimitConsumedMessagesExtension implements ExtensionInterface
+class LimitConsumedMessagesExtension implements PreConsumeExtensionInterface, PostMessageReceivedExtensionInterface
 {
-    use EmptyExtensionTrait;
-
     /**
      * @var int
      */
@@ -20,54 +20,41 @@ class LimitConsumedMessagesExtension implements ExtensionInterface
      */
     protected $messageConsumed;
 
-    /**
-     * @param int $messageLimit
-     */
-    public function __construct($messageLimit)
+    public function __construct(int $messageLimit)
     {
-        if (false == is_int($messageLimit)) {
-            throw new \InvalidArgumentException(sprintf(
-                'Expected message limit is int but got: "%s"',
-                is_object($messageLimit) ? get_class($messageLimit) : gettype($messageLimit)
-            ));
-        }
-
         $this->messageLimit = $messageLimit;
         $this->messageConsumed = 0;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onBeforeReceive(Context $context)
+    public function onPreConsume(PreConsume $context): void
     {
         // this is added here to handle an edge case. when a user sets zero as limit.
-        $this->checkMessageLimit($context);
+        if ($this->shouldBeStopped($context->getLogger())) {
+            $context->interruptExecution();
+        }
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onPostReceived(Context $context)
+    public function onPostMessageReceived(PostMessageReceived $context): void
     {
         ++$this->messageConsumed;
 
-        $this->checkMessageLimit($context);
+        if ($this->shouldBeStopped($context->getLogger())) {
+            $context->interruptExecution();
+        }
     }
 
-    /**
-     * @param Context $context
-     */
-    protected function checkMessageLimit(Context $context)
+    protected function shouldBeStopped(LoggerInterface $logger): bool
     {
         if ($this->messageConsumed >= $this->messageLimit) {
-            $context->getLogger()->debug(sprintf(
+            $logger->debug(sprintf(
                 '[LimitConsumedMessagesExtension] Message consumption is interrupted since the message limit reached.'.
                 ' limit: "%s"',
                 $this->messageLimit
             ));
 
-            $context->setExecutionInterrupted(true);
+            return true;
         }
+
+        return false;
     }
 }
diff --git a/Consumption/Extension/LimitConsumerMemoryExtension.php b/Consumption/Extension/LimitConsumerMemoryExtension.php
index c03686f..7edbf23 100644
--- a/Consumption/Extension/LimitConsumerMemoryExtension.php
+++ b/Consumption/Extension/LimitConsumerMemoryExtension.php
@@ -2,14 +2,16 @@
 
 namespace Enqueue\Consumption\Extension;
 
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\EmptyExtensionTrait;
-use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\Context\PostConsume;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\PreConsume;
+use Enqueue\Consumption\PostConsumeExtensionInterface;
+use Enqueue\Consumption\PostMessageReceivedExtensionInterface;
+use Enqueue\Consumption\PreConsumeExtensionInterface;
+use Psr\Log\LoggerInterface;
 
-class LimitConsumerMemoryExtension implements ExtensionInterface
+class LimitConsumerMemoryExtension implements PreConsumeExtensionInterface, PostMessageReceivedExtensionInterface, PostConsumeExtensionInterface
 {
-    use EmptyExtensionTrait;
-
     /**
      * @var int
      */
@@ -21,53 +23,46 @@ class LimitConsumerMemoryExtension implements ExtensionInterface
     public function __construct($memoryLimit)
     {
         if (false == is_int($memoryLimit)) {
-            throw new \InvalidArgumentException(sprintf(
-                'Expected memory limit is int but got: "%s"',
-                is_object($memoryLimit) ? get_class($memoryLimit) : gettype($memoryLimit)
-            ));
+            throw new \InvalidArgumentException(sprintf('Expected memory limit is int but got: "%s"', is_object($memoryLimit) ? $memoryLimit::class : gettype($memoryLimit)));
         }
 
         $this->memoryLimit = $memoryLimit * 1024 * 1024;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onBeforeReceive(Context $context)
+    public function onPreConsume(PreConsume $context): void
     {
-        $this->checkMemory($context);
+        if ($this->shouldBeStopped($context->getLogger())) {
+            $context->interruptExecution();
+        }
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onPostReceived(Context $context)
+    public function onPostMessageReceived(PostMessageReceived $context): void
     {
-        $this->checkMemory($context);
+        if ($this->shouldBeStopped($context->getLogger())) {
+            $context->interruptExecution();
+        }
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onIdle(Context $context)
+    public function onPostConsume(PostConsume $context): void
     {
-        $this->checkMemory($context);
+        if ($this->shouldBeStopped($context->getLogger())) {
+            $context->interruptExecution();
+        }
     }
 
-    /**
-     * @param Context $context
-     */
-    protected function checkMemory(Context $context)
+    protected function shouldBeStopped(LoggerInterface $logger): bool
     {
         $memoryUsage = memory_get_usage(true);
         if ($memoryUsage >= $this->memoryLimit) {
-            $context->getLogger()->debug(sprintf(
+            $logger->debug(sprintf(
                 '[LimitConsumerMemoryExtension] Interrupt execution as memory limit reached. limit: "%s", used: "%s"',
                 $this->memoryLimit,
                 $memoryUsage
             ));
 
-            $context->setExecutionInterrupted(true);
+            return true;
         }
+
+        return false;
     }
 }
diff --git a/Consumption/Extension/LimitConsumptionTimeExtension.php b/Consumption/Extension/LimitConsumptionTimeExtension.php
index 65221f5..1953aa2 100644
--- a/Consumption/Extension/LimitConsumptionTimeExtension.php
+++ b/Consumption/Extension/LimitConsumptionTimeExtension.php
@@ -2,66 +2,61 @@
 
 namespace Enqueue\Consumption\Extension;
 
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\EmptyExtensionTrait;
-use Enqueue\Consumption\ExtensionInterface;
-
-class LimitConsumptionTimeExtension implements ExtensionInterface
+use Enqueue\Consumption\Context\PostConsume;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\PreConsume;
+use Enqueue\Consumption\PostConsumeExtensionInterface;
+use Enqueue\Consumption\PostMessageReceivedExtensionInterface;
+use Enqueue\Consumption\PreConsumeExtensionInterface;
+use Psr\Log\LoggerInterface;
+
+class LimitConsumptionTimeExtension implements PreConsumeExtensionInterface, PostConsumeExtensionInterface, PostMessageReceivedExtensionInterface
 {
-    use EmptyExtensionTrait;
-
     /**
      * @var \DateTime
      */
     protected $timeLimit;
 
-    /**
-     * @param \DateTime $timeLimit
-     */
     public function __construct(\DateTime $timeLimit)
     {
         $this->timeLimit = $timeLimit;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onBeforeReceive(Context $context)
+    public function onPreConsume(PreConsume $context): void
     {
-        $this->checkTime($context);
+        if ($this->shouldBeStopped($context->getLogger())) {
+            $context->interruptExecution();
+        }
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onIdle(Context $context)
+    public function onPostConsume(PostConsume $context): void
     {
-        $this->checkTime($context);
+        if ($this->shouldBeStopped($context->getLogger())) {
+            $context->interruptExecution();
+        }
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onPostReceived(Context $context)
+    public function onPostMessageReceived(PostMessageReceived $context): void
     {
-        $this->checkTime($context);
+        if ($this->shouldBeStopped($context->getLogger())) {
+            $context->interruptExecution();
+        }
     }
 
-    /**
-     * @param Context $context
-     */
-    protected function checkTime(Context $context)
+    protected function shouldBeStopped(LoggerInterface $logger): bool
     {
         $now = new \DateTime();
         if ($now >= $this->timeLimit) {
-            $context->getLogger()->debug(sprintf(
+            $logger->debug(sprintf(
                 '[LimitConsumptionTimeExtension] Execution interrupted as limit time has passed.'.
                 ' now: "%s", time-limit: "%s"',
-                $now->format(DATE_ISO8601),
-                $this->timeLimit->format(DATE_ISO8601)
+                $now->format(\DATE_ISO8601),
+                $this->timeLimit->format(\DATE_ISO8601)
             ));
 
-            $context->setExecutionInterrupted(true);
+            return true;
         }
+
+        return false;
     }
 }
diff --git a/Consumption/Extension/LogExtension.php b/Consumption/Extension/LogExtension.php
new file mode 100644
index 0000000..14383c4
--- /dev/null
+++ b/Consumption/Extension/LogExtension.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Enqueue\Consumption\Extension;
+
+use Enqueue\Consumption\Context\End;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\Start;
+use Enqueue\Consumption\EndExtensionInterface;
+use Enqueue\Consumption\MessageReceivedExtensionInterface;
+use Enqueue\Consumption\PostMessageReceivedExtensionInterface;
+use Enqueue\Consumption\Result;
+use Enqueue\Consumption\StartExtensionInterface;
+use Enqueue\Util\Stringify;
+use Psr\Log\LogLevel;
+
+class LogExtension implements StartExtensionInterface, MessageReceivedExtensionInterface, PostMessageReceivedExtensionInterface, EndExtensionInterface
+{
+    public function onStart(Start $context): void
+    {
+        $context->getLogger()->debug('Consumption has started');
+    }
+
+    public function onEnd(End $context): void
+    {
+        $context->getLogger()->debug('Consumption has ended');
+    }
+
+    public function onMessageReceived(MessageReceived $context): void
+    {
+        $message = $context->getMessage();
+
+        $context->getLogger()->debug("Received from {queueName}\t{body}", [
+            'queueName' => $context->getConsumer()->getQueue()->getQueueName(),
+            'redelivered' => $message->isRedelivered(),
+            'body' => Stringify::that($message->getBody()),
+            'properties' => Stringify::that($message->getProperties()),
+            'headers' => Stringify::that($message->getHeaders()),
+        ]);
+    }
+
+    public function onPostMessageReceived(PostMessageReceived $context): void
+    {
+        $message = $context->getMessage();
+        $queue = $context->getConsumer()->getQueue();
+        $result = $context->getResult();
+
+        $reason = '';
+        $logMessage = "Processed from {queueName}\t{body}\t{result}";
+        if ($result instanceof Result && $result->getReason()) {
+            $reason = $result->getReason();
+            $logMessage .= ' {reason}';
+        }
+        $logContext = [
+            'result' => str_replace('enqueue.', '', $result),
+            'reason' => $reason,
+            'queueName' => $queue->getQueueName(),
+            'body' => Stringify::that($message->getBody()),
+            'properties' => Stringify::that($message->getProperties()),
+            'headers' => Stringify::that($message->getHeaders()),
+        ];
+
+        $logLevel = Result::REJECT == ((string) $result) ? LogLevel::ERROR : LogLevel::INFO;
+
+        $context->getLogger()->log($logLevel, $logMessage, $logContext);
+    }
+}
diff --git a/Consumption/Extension/LoggerExtension.php b/Consumption/Extension/LoggerExtension.php
index 1322c28..90e92be 100644
--- a/Consumption/Extension/LoggerExtension.php
+++ b/Consumption/Extension/LoggerExtension.php
@@ -2,81 +2,30 @@
 
 namespace Enqueue\Consumption\Extension;
 
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\EmptyExtensionTrait;
-use Enqueue\Consumption\ExtensionInterface;
-use Enqueue\Consumption\Result;
-use Interop\Queue\PsrMessage;
+use Enqueue\Consumption\Context\InitLogger;
+use Enqueue\Consumption\InitLoggerExtensionInterface;
 use Psr\Log\LoggerInterface;
 
-class LoggerExtension implements ExtensionInterface
+class LoggerExtension implements InitLoggerExtensionInterface
 {
-    use EmptyExtensionTrait;
-
     /**
      * @var LoggerInterface
      */
     private $logger;
 
-    /**
-     * @param LoggerInterface $logger
-     */
     public function __construct(LoggerInterface $logger)
     {
         $this->logger = $logger;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onStart(Context $context)
-    {
-        $context->setLogger($this->logger);
-        $this->logger->debug(sprintf('Set context\'s logger %s', get_class($this->logger)));
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function onPostReceived(Context $context)
+    public function onInitLogger(InitLogger $context): void
     {
-        if (false == $context->getResult() instanceof Result) {
-            return;
-        }
-
-        /** @var $result Result */
-        $result = $context->getResult();
+        $previousLogger = $context->getLogger();
 
-        switch ($result->getStatus()) {
-            case Result::REJECT:
-            case Result::REQUEUE:
-                if ($result->getReason()) {
-                    $this->logger->error($result->getReason(), $this->messageToLogContext($context->getPsrMessage()));
-                }
+        if ($previousLogger !== $this->logger) {
+            $context->changeLogger($this->logger);
 
-                break;
-            case Result::ACK:
-                if ($result->getReason()) {
-                    $this->logger->info($result->getReason(), $this->messageToLogContext($context->getPsrMessage()));
-                }
-
-                break;
-            default:
-                throw new \LogicException(sprintf('Got unexpected message result. "%s"', $result->getStatus()));
+            $this->logger->debug(sprintf('Change logger from "%s" to "%s"', $previousLogger::class, get_class($this->logger)));
         }
     }
-
-    /**
-     * @param PsrMessage $message
-     *
-     * @return array
-     */
-    private function messageToLogContext(PsrMessage $message)
-    {
-        return [
-            'body' => $message->getBody(),
-            'headers' => $message->getHeaders(),
-            'properties' => $message->getProperties(),
-        ];
-    }
 }
diff --git a/Consumption/Extension/NicenessExtension.php b/Consumption/Extension/NicenessExtension.php
new file mode 100644
index 0000000..436a8ec
--- /dev/null
+++ b/Consumption/Extension/NicenessExtension.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Enqueue\Consumption\Extension;
+
+use Enqueue\Consumption\Context\Start;
+use Enqueue\Consumption\StartExtensionInterface;
+
+class NicenessExtension implements StartExtensionInterface
+{
+    /**
+     * @var int
+     */
+    protected $niceness = 0;
+
+    /**
+     * @param int $niceness
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function __construct($niceness)
+    {
+        if (false === is_int($niceness)) {
+            throw new \InvalidArgumentException(sprintf('Expected niceness value is int but got: "%s"', is_object($niceness) ? $niceness::class : gettype($niceness)));
+        }
+
+        $this->niceness = $niceness;
+    }
+
+    public function onStart(Start $context): void
+    {
+        if (0 !== $this->niceness) {
+            $changed = @proc_nice($this->niceness);
+            if (!$changed) {
+                throw new \InvalidArgumentException(sprintf('Cannot change process niceness, got warning: "%s"', error_get_last()['message']));
+            }
+        }
+    }
+}
diff --git a/Consumption/Extension/ReplyExtension.php b/Consumption/Extension/ReplyExtension.php
index 0d7a76e..c1ac19b 100644
--- a/Consumption/Extension/ReplyExtension.php
+++ b/Consumption/Extension/ReplyExtension.php
@@ -2,21 +2,15 @@
 
 namespace Enqueue\Consumption\Extension;
 
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\EmptyExtensionTrait;
-use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\PostMessageReceivedExtensionInterface;
 use Enqueue\Consumption\Result;
 
-class ReplyExtension implements ExtensionInterface
+class ReplyExtension implements PostMessageReceivedExtensionInterface
 {
-    use EmptyExtensionTrait;
-
-    /**
-     * {@inheritdoc}
-     */
-    public function onPostReceived(Context $context)
+    public function onPostMessageReceived(PostMessageReceived $context): void
     {
-        $replyTo = $context->getPsrMessage()->getReplyTo();
+        $replyTo = $context->getMessage()->getReplyTo();
         if (false == $replyTo) {
             return;
         }
@@ -31,13 +25,13 @@ public function onPostReceived(Context $context)
             return;
         }
 
-        $correlationId = $context->getPsrMessage()->getCorrelationId();
+        $correlationId = $context->getMessage()->getCorrelationId();
         $replyMessage = clone $result->getReply();
         $replyMessage->setCorrelationId($correlationId);
 
-        $replyQueue = $context->getPsrContext()->createQueue($replyTo);
+        $replyQueue = $context->getContext()->createQueue($replyTo);
 
         $context->getLogger()->debug(sprintf('[ReplyExtension] Send reply to "%s"', $replyTo));
-        $context->getPsrContext()->createProducer()->send($replyQueue, $replyMessage);
+        $context->getContext()->createProducer()->send($replyQueue, $replyMessage);
     }
 }
diff --git a/Consumption/Extension/SignalExtension.php b/Consumption/Extension/SignalExtension.php
index a3bab85..8ea5307 100644
--- a/Consumption/Extension/SignalExtension.php
+++ b/Consumption/Extension/SignalExtension.php
@@ -2,113 +2,99 @@
 
 namespace Enqueue\Consumption\Extension;
 
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\EmptyExtensionTrait;
+use Enqueue\Consumption\Context\PostConsume;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\PreConsume;
+use Enqueue\Consumption\Context\Start;
 use Enqueue\Consumption\Exception\LogicException;
-use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\PostConsumeExtensionInterface;
+use Enqueue\Consumption\PostMessageReceivedExtensionInterface;
+use Enqueue\Consumption\PreConsumeExtensionInterface;
+use Enqueue\Consumption\StartExtensionInterface;
 use Psr\Log\LoggerInterface;
 
-class SignalExtension implements ExtensionInterface
+class SignalExtension implements StartExtensionInterface, PreConsumeExtensionInterface, PostMessageReceivedExtensionInterface, PostConsumeExtensionInterface
 {
-    use EmptyExtensionTrait;
-
     /**
      * @var bool
      */
     protected $interruptConsumption = false;
 
     /**
-     * @var LoggerInterface
+     * @var LoggerInterface|null
      */
     protected $logger;
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onStart(Context $context)
+    public function onStart(Start $context): void
     {
         if (false == extension_loaded('pcntl')) {
             throw new LogicException('The pcntl extension is required in order to catch signals.');
         }
 
-        pcntl_signal(SIGTERM, [$this, 'handleSignal']);
-        pcntl_signal(SIGQUIT, [$this, 'handleSignal']);
-        pcntl_signal(SIGINT, [$this, 'handleSignal']);
+        pcntl_async_signals(true);
 
+        pcntl_signal(\SIGTERM, [$this, 'handleSignal']);
+        pcntl_signal(\SIGQUIT, [$this, 'handleSignal']);
+        pcntl_signal(\SIGINT, [$this, 'handleSignal']);
+
+        $this->logger = $context->getLogger();
         $this->interruptConsumption = false;
     }
 
-    /**
-     * @param Context $context
-     */
-    public function onBeforeReceive(Context $context)
+    public function onPreConsume(PreConsume $context): void
     {
         $this->logger = $context->getLogger();
 
-        pcntl_signal_dispatch();
-
-        $this->interruptExecutionIfNeeded($context);
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function onPreReceived(Context $context)
-    {
-        $this->interruptExecutionIfNeeded($context);
+        if ($this->shouldBeStopped($context->getLogger())) {
+            $context->interruptExecution();
+        }
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function onPostReceived(Context $context)
+    public function onPostMessageReceived(PostMessageReceived $context): void
     {
-        pcntl_signal_dispatch();
-
-        $this->interruptExecutionIfNeeded($context);
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function onIdle(Context $context)
-    {
-        pcntl_signal_dispatch();
-
-        $this->interruptExecutionIfNeeded($context);
+        if ($this->shouldBeStopped($context->getLogger())) {
+            $context->interruptExecution();
+        }
     }
 
-    /**
-     * @param Context $context
-     */
-    public function interruptExecutionIfNeeded(Context $context)
+    public function onPostConsume(PostConsume $context): void
     {
-        if (false == $context->isExecutionInterrupted() && $this->interruptConsumption) {
-            $this->logger->debug('[SignalExtension] Interrupt execution');
-            $context->setExecutionInterrupted($this->interruptConsumption);
-
-            $this->interruptConsumption = false;
+        if ($this->shouldBeStopped($context->getLogger())) {
+            $context->interruptExecution();
         }
     }
 
-    /**
-     * @param int $signal
-     */
-    public function handleSignal($signal)
+    public function handleSignal(int $signal): void
     {
         if ($this->logger) {
             $this->logger->debug(sprintf('[SignalExtension] Caught signal: %s', $signal));
         }
 
         switch ($signal) {
-            case SIGTERM:  // 15 : supervisor default stop
-            case SIGQUIT:  // 3  : kill -s QUIT
-            case SIGINT:   // 2  : ctrl+c
-                $this->logger->debug('[SignalExtension] Interrupt consumption');
+            case \SIGTERM:  // 15 : supervisor default stop
+            case \SIGQUIT:  // 3  : kill -s QUIT
+            case \SIGINT:   // 2  : ctrl+c
+                if ($this->logger) {
+                    $this->logger->debug('[SignalExtension] Interrupt consumption');
+                }
+
                 $this->interruptConsumption = true;
                 break;
             default:
                 break;
         }
     }
+
+    private function shouldBeStopped(LoggerInterface $logger): bool
+    {
+        if ($this->interruptConsumption) {
+            $logger->debug('[SignalExtension] Interrupt execution');
+
+            $this->interruptConsumption = false;
+
+            return true;
+        }
+
+        return false;
+    }
 }
diff --git a/Consumption/ExtensionInterface.php b/Consumption/ExtensionInterface.php
index 2a5d7bb..326a98f 100644
--- a/Consumption/ExtensionInterface.php
+++ b/Consumption/ExtensionInterface.php
@@ -2,65 +2,6 @@
 
 namespace Enqueue\Consumption;
 
-interface ExtensionInterface
+interface ExtensionInterface extends StartExtensionInterface, PreSubscribeExtensionInterface, PreConsumeExtensionInterface, MessageReceivedExtensionInterface, PostMessageReceivedExtensionInterface, MessageResultExtensionInterface, ProcessorExceptionExtensionInterface, PostConsumeExtensionInterface, EndExtensionInterface, InitLoggerExtensionInterface
 {
-    /**
-     * Executed only once at the very begining of the consumption.
-     * At this stage the context does not contain processor, consumer and queue.
-     *
-     * @param Context $context
-     */
-    public function onStart(Context $context);
-
-    /**
-     * Executed at every new cycle before we asked a broker for a new message.
-     * At this stage the context already contains processor, consumer and queue.
-     * The consumption could be interrupted at this step.
-     *
-     * @param Context $context
-     */
-    public function onBeforeReceive(Context $context);
-
-    /**
-     * Executed when a new message is received from a broker but before it was passed to processor
-     * The context contains a message.
-     * The extension may set a status. If the status is set the exception is thrown
-     * The consumption could be interrupted at this step but it exits after the message is processed.
-     *
-     * @param Context $context
-     */
-    public function onPreReceived(Context $context);
-
-    /**
-     * Executed when a message is processed by a processor or a result was set in onPreReceived method.
-     * BUT before the message status was sent to the broker
-     * The consumption could be interrupted at this step but it exits after the message is processed.
-     *
-     * @param Context $context
-     */
-    public function onResult(Context $context);
-
-    /**
-     * Executed when a message is processed by a processor.
-     * The context contains a status, which could not be changed.
-     * The consumption could be interrupted at this step but it exits after the message is processed.
-     *
-     * @param Context $context
-     */
-    public function onPostReceived(Context $context);
-
-    /**
-     * Called each time at the end of the cycle if nothing was done.
-     *
-     * @param Context $context
-     */
-    public function onIdle(Context $context);
-
-    /**
-     * Called when the consumption was interrupted by an extension or exception
-     * In case of exception it will be present in the context.
-     *
-     * @param Context $context
-     */
-    public function onInterrupted(Context $context);
 }
diff --git a/Consumption/FallbackSubscriptionConsumer.php b/Consumption/FallbackSubscriptionConsumer.php
new file mode 100644
index 0000000..15e2f27
--- /dev/null
+++ b/Consumption/FallbackSubscriptionConsumer.php
@@ -0,0 +1,109 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Interop\Queue\Consumer;
+use Interop\Queue\SubscriptionConsumer;
+
+final class FallbackSubscriptionConsumer implements SubscriptionConsumer
+{
+    /**
+     * an item contains an array: [Consumer $consumer, callable $callback];.
+     * an item key is a queue name.
+     *
+     * @var array
+     */
+    private $subscribers;
+
+    /**
+     * @var int
+     */
+    private $idleTime = 0;
+
+    public function __construct()
+    {
+        $this->subscribers = [];
+    }
+
+    public function consume(int $timeoutMs = 0): void
+    {
+        if (!$subscriberCount = \count($this->subscribers)) {
+            throw new \LogicException('No subscribers');
+        }
+
+        $timeout = $timeoutMs / 1000;
+        $endAt = microtime(true) + $timeout;
+
+        while (true) {
+            /**
+             * @var string
+             * @var Consumer $consumer
+             * @var callable $processor
+             */
+            foreach ($this->subscribers as $queueName => list($consumer, $callback)) {
+                $message = 1 === $subscriberCount ? $consumer->receive($timeoutMs) : $consumer->receiveNoWait();
+
+                if ($message) {
+                    if (false === call_user_func($callback, $message, $consumer)) {
+                        return;
+                    }
+                } elseif (1 !== $subscriberCount) {
+                    if ($timeout && microtime(true) >= $endAt) {
+                        return;
+                    }
+
+                    $this->idleTime && usleep($this->idleTime);
+                }
+
+                if ($timeout && microtime(true) >= $endAt) {
+                    return;
+                }
+            }
+        }
+    }
+
+    public function subscribe(Consumer $consumer, callable $callback): void
+    {
+        $queueName = $consumer->getQueue()->getQueueName();
+        if (array_key_exists($queueName, $this->subscribers)) {
+            if ($this->subscribers[$queueName][0] === $consumer && $this->subscribers[$queueName][1] === $callback) {
+                return;
+            }
+
+            throw new \InvalidArgumentException(sprintf('There is a consumer subscribed to queue: "%s"', $queueName));
+        }
+
+        $this->subscribers[$queueName] = [$consumer, $callback];
+    }
+
+    public function unsubscribe(Consumer $consumer): void
+    {
+        if (false == array_key_exists($consumer->getQueue()->getQueueName(), $this->subscribers)) {
+            return;
+        }
+
+        if ($this->subscribers[$consumer->getQueue()->getQueueName()][0] !== $consumer) {
+            return;
+        }
+
+        unset($this->subscribers[$consumer->getQueue()->getQueueName()]);
+    }
+
+    public function unsubscribeAll(): void
+    {
+        $this->subscribers = [];
+    }
+
+    public function getIdleTime(): int
+    {
+        return $this->idleTime;
+    }
+
+    /**
+     * The time in milliseconds the consumer waits if no message has been received.
+     */
+    public function setIdleTime(int $idleTime): void
+    {
+        $this->idleTime = $idleTime;
+    }
+}
diff --git a/Consumption/InitLoggerExtensionInterface.php b/Consumption/InitLoggerExtensionInterface.php
new file mode 100644
index 0000000..936e32d
--- /dev/null
+++ b/Consumption/InitLoggerExtensionInterface.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Enqueue\Consumption\Context\InitLogger;
+
+interface InitLoggerExtensionInterface
+{
+    /**
+     * Executed only once at the very beginning of the QueueConsumer::consume method call.
+     * BEFORE onStart extension method.
+     */
+    public function onInitLogger(InitLogger $context): void;
+}
diff --git a/Consumption/MessageReceivedExtensionInterface.php b/Consumption/MessageReceivedExtensionInterface.php
new file mode 100644
index 0000000..10afc9c
--- /dev/null
+++ b/Consumption/MessageReceivedExtensionInterface.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Enqueue\Consumption\Context\MessageReceived;
+
+interface MessageReceivedExtensionInterface
+{
+    /**
+     * Executed as soon as a a message is received, before it is passed to a processor
+     * The extension may set a result. If the result is set the processor is not called
+     * The processor could be changed or decorated at this point.
+     */
+    public function onMessageReceived(MessageReceived $context): void;
+}
diff --git a/Consumption/MessageResultExtensionInterface.php b/Consumption/MessageResultExtensionInterface.php
new file mode 100644
index 0000000..01d9e53
--- /dev/null
+++ b/Consumption/MessageResultExtensionInterface.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Enqueue\Consumption\Context\MessageResult;
+
+interface MessageResultExtensionInterface
+{
+    /**
+     * Executed when a message is processed by a processor or a result was set in onMessageReceived extension method.
+     * BEFORE the message status was sent to the broker
+     * The result could be changed at this point.
+     */
+    public function onResult(MessageResult $context): void;
+}
diff --git a/Consumption/PostConsumeExtensionInterface.php b/Consumption/PostConsumeExtensionInterface.php
new file mode 100644
index 0000000..a8706f6
--- /dev/null
+++ b/Consumption/PostConsumeExtensionInterface.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Enqueue\Consumption\Context\PostConsume;
+
+interface PostConsumeExtensionInterface
+{
+    /**
+     * The method is called after SubscriptionConsumer::consume method exits.
+     * The consumption could be interrupted at this point.
+     */
+    public function onPostConsume(PostConsume $context): void;
+}
diff --git a/Consumption/PostMessageReceivedExtensionInterface.php b/Consumption/PostMessageReceivedExtensionInterface.php
new file mode 100644
index 0000000..febee4a
--- /dev/null
+++ b/Consumption/PostMessageReceivedExtensionInterface.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Enqueue\Consumption\Context\PostMessageReceived;
+
+interface PostMessageReceivedExtensionInterface
+{
+    /**
+     * Executed at the very end of consumption callback. The message has already been acknowledged.
+     * The message result could not be changed.
+     * The consumption could be interrupted at this point.
+     */
+    public function onPostMessageReceived(PostMessageReceived $context): void;
+}
diff --git a/Consumption/PreConsumeExtensionInterface.php b/Consumption/PreConsumeExtensionInterface.php
new file mode 100644
index 0000000..0b13fd8
--- /dev/null
+++ b/Consumption/PreConsumeExtensionInterface.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Enqueue\Consumption\Context\PreConsume;
+
+interface PreConsumeExtensionInterface
+{
+    /**
+     * Executed at every new cycle before calling SubscriptionConsumer::consume method.
+     * The consumption could be interrupted at this step.
+     */
+    public function onPreConsume(PreConsume $context): void;
+}
diff --git a/Consumption/PreSubscribeExtensionInterface.php b/Consumption/PreSubscribeExtensionInterface.php
new file mode 100644
index 0000000..a6899dd
--- /dev/null
+++ b/Consumption/PreSubscribeExtensionInterface.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Enqueue\Consumption\Context\PreSubscribe;
+
+interface PreSubscribeExtensionInterface
+{
+    /**
+     * The method is called for each BoundProcessor before calling SubscriptionConsumer::subscribe method.
+     */
+    public function onPreSubscribe(PreSubscribe $context): void;
+}
diff --git a/Consumption/ProcessorExceptionExtensionInterface.php b/Consumption/ProcessorExceptionExtensionInterface.php
new file mode 100644
index 0000000..dc17d94
--- /dev/null
+++ b/Consumption/ProcessorExceptionExtensionInterface.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Enqueue\Consumption\Context\ProcessorException;
+
+interface ProcessorExceptionExtensionInterface
+{
+    /**
+     * Execute if a processor throws an exception.
+     * The result could be set, if result is not set the exception is thrown again.
+     */
+    public function onProcessorException(ProcessorException $context): void;
+}
diff --git a/Consumption/QueueConsumer.php b/Consumption/QueueConsumer.php
index 561314d..f43f5c4 100644
--- a/Consumption/QueueConsumer.php
+++ b/Consumption/QueueConsumer.php
@@ -2,127 +2,108 @@
 
 namespace Enqueue\Consumption;
 
-use Enqueue\AmqpExt\AmqpConsumer;
-use Enqueue\AmqpExt\AmqpContext;
-use Enqueue\Consumption\Exception\ConsumptionInterruptedException;
+use Enqueue\Consumption\Context\End;
+use Enqueue\Consumption\Context\InitLogger;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\Context\MessageResult;
+use Enqueue\Consumption\Context\PostConsume;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\PreConsume;
+use Enqueue\Consumption\Context\PreSubscribe;
+use Enqueue\Consumption\Context\ProcessorException;
+use Enqueue\Consumption\Context\Start;
 use Enqueue\Consumption\Exception\InvalidArgumentException;
 use Enqueue\Consumption\Exception\LogicException;
-use Enqueue\Util\VarExport;
-use Interop\Amqp\AmqpMessage;
-use Interop\Queue\PsrConsumer;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrProcessor;
-use Interop\Queue\PsrQueue;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context as InteropContext;
+use Interop\Queue\Exception\SubscriptionConsumerNotSupportedException;
+use Interop\Queue\Message;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Processor;
+use Interop\Queue\Queue as InteropQueue;
+use Interop\Queue\SubscriptionConsumer;
+use Psr\Log\LoggerInterface;
 use Psr\Log\NullLogger;
 
-class QueueConsumer
+final class QueueConsumer implements QueueConsumerInterface
 {
     /**
-     * @var PsrContext
+     * @var InteropContext
      */
-    private $psrContext;
+    private $interopContext;
 
     /**
-     * @var ExtensionInterface|ChainExtension|null
+     * @var ExtensionInterface
      */
-    private $extension;
+    private $staticExtension;
 
     /**
-     * [
-     *   [PsrQueue, PsrProcessor],
-     * ].
-     *
-     * @var array
+     * @var BoundProcessor[]
      */
     private $boundProcessors;
 
     /**
-     * @var int|float in milliseconds
+     * @var int in milliseconds
      */
-    private $idleTimeout;
+    private $receiveTimeout;
 
     /**
-     * @var int|float in milliseconds
+     * @var LoggerInterface
      */
-    private $receiveTimeout;
+    private $logger;
 
     /**
-     * @param PsrContext                             $psrContext
-     * @param ExtensionInterface|ChainExtension|null $extension
-     * @param int|float                              $idleTimeout    the time in milliseconds queue consumer waits if no message received
-     * @param int|float                              $receiveTimeout the time in milliseconds queue consumer waits for a message (10 ms by default)
+     * @var SubscriptionConsumer
+     */
+    private $fallbackSubscriptionConsumer;
+
+    /**
+     * @param BoundProcessor[] $boundProcessors
+     * @param int              $receiveTimeout  the time in milliseconds queue consumer waits for a message (10000 ms by default)
      */
     public function __construct(
-        PsrContext $psrContext,
-        ExtensionInterface $extension = null,
-        $idleTimeout = 0,
-        $receiveTimeout = 10
+        InteropContext $interopContext,
+        ?ExtensionInterface $extension = null,
+        array $boundProcessors = [],
+        ?LoggerInterface $logger = null,
+        int $receiveTimeout = 10000,
     ) {
-        $this->psrContext = $psrContext;
-        $this->extension = $extension;
-        $this->idleTimeout = $idleTimeout;
+        $this->interopContext = $interopContext;
         $this->receiveTimeout = $receiveTimeout;
 
+        $this->staticExtension = $extension ?: new ChainExtension([]);
+        $this->logger = $logger ?: new NullLogger();
+
         $this->boundProcessors = [];
-    }
+        array_walk($boundProcessors, function (BoundProcessor $processor) {
+            $this->boundProcessors[] = $processor;
+        });
 
-    /**
-     * @param int $timeout
-     */
-    public function setIdleTimeout($timeout)
-    {
-        $this->idleTimeout = (int) $timeout;
+        $this->fallbackSubscriptionConsumer = new FallbackSubscriptionConsumer();
     }
 
-    /**
-     * @return int
-     */
-    public function getIdleTimeout()
+    public function setReceiveTimeout(int $timeout): void
     {
-        return $this->idleTimeout;
+        $this->receiveTimeout = $timeout;
     }
 
-    /**
-     * @param int $timeout
-     */
-    public function setReceiveTimeout($timeout)
-    {
-        $this->receiveTimeout = (int) $timeout;
-    }
-
-    /**
-     * @return int
-     */
-    public function getReceiveTimeout()
+    public function getReceiveTimeout(): int
     {
         return $this->receiveTimeout;
     }
 
-    /**
-     * @return PsrContext
-     */
-    public function getPsrContext()
+    public function getContext(): InteropContext
     {
-        return $this->psrContext;
+        return $this->interopContext;
     }
 
-    /**
-     * @param PsrQueue|string       $queue
-     * @param PsrProcessor|callable $processor
-     *
-     * @return QueueConsumer
-     */
-    public function bind($queue, $processor)
+    public function bind($queue, Processor $processor): QueueConsumerInterface
     {
         if (is_string($queue)) {
-            $queue = $this->psrContext->createQueue($queue);
-        }
-        if (is_callable($processor)) {
-            $processor = new CallbackProcessor($processor);
+            $queue = $this->interopContext->createQueue($queue);
         }
 
-        InvalidArgumentException::assertInstanceOf($queue, PsrQueue::class);
-        InvalidArgumentException::assertInstanceOf($processor, PsrProcessor::class);
+        InvalidArgumentException::assertInstanceOf($queue, InteropQueue::class);
 
         if (empty($queue->getQueueName())) {
             throw new LogicException('The queue name must be not empty.');
@@ -131,150 +112,99 @@ public function bind($queue, $processor)
             throw new LogicException(sprintf('The queue was already bound. Queue: %s', $queue->getQueueName()));
         }
 
-        $this->boundProcessors[$queue->getQueueName()] = [$queue, $processor];
+        $this->boundProcessors[$queue->getQueueName()] = new BoundProcessor($queue, $processor);
 
         return $this;
     }
 
-    /**
-     * Runtime extension - is an extension or a collection of extensions which could be set on runtime.
-     * Here's a good example: @see LimitsExtensionsCommandTrait.
-     *
-     * @param ExtensionInterface|ChainExtension|null $runtimeExtension
-     *
-     * @throws \Exception
-     */
-    public function consume(ExtensionInterface $runtimeExtension = null)
+    public function bindCallback($queue, callable $processor): QueueConsumerInterface
     {
-        /** @var PsrConsumer[] $consumers */
-        $consumers = [];
-        /** @var PsrQueue $queue */
-        foreach ($this->boundProcessors as list($queue, $processor)) {
-            $consumers[$queue->getQueueName()] = $this->psrContext->createConsumer($queue);
-        }
+        return $this->bind($queue, new CallbackProcessor($processor));
+    }
 
-        foreach ($consumers as $consumer) {
-            if ($consumer instanceof AmqpConsumer) {
-                $consumer->basicConsume($this->receiveTimeout, null);
-            }
-        }
+    public function consume(?ExtensionInterface $runtimeExtension = null): void
+    {
+        $extension = $runtimeExtension ?
+            new ChainExtension([$this->staticExtension, $runtimeExtension]) :
+            $this->staticExtension
+        ;
 
-        $extension = $this->extension ?: new ChainExtension([]);
-        if ($runtimeExtension) {
-            $extension = new ChainExtension([$extension, $runtimeExtension]);
-        }
+        $initLogger = new InitLogger($this->logger);
+        $extension->onInitLogger($initLogger);
 
-        $context = new Context($this->psrContext);
-        $extension->onStart($context);
+        $this->logger = $initLogger->getLogger();
 
-        $logger = $context->getLogger() ?: new NullLogger();
-        $logger->info('Start consuming');
+        $startTime = (int) (microtime(true) * 1000);
 
-        while (true) {
-            try {
-                if ($this->psrContext instanceof AmqpContext) {
-                    reset($consumers);
-                    /** @var AmqpConsumer $consumer */
-                    $consumer = current($consumers);
-                    $consumer->basicConsume($this->receiveTimeout, function (AmqpMessage $message, AmqpConsumer $consumer) use ($extension, $logger) {
-                        $currentProcessor = null;
-
-                        /** @var PsrQueue $queue */
-                        foreach ($this->boundProcessors as list($queue, $processor)) {
-                            if ($queue->getQueueName() === $consumer->getQueue()->getQueueName()) {
-                                $currentProcessor = $processor;
-                            }
-                        }
-
-                        if (false == $currentProcessor) {
-                            throw new \LogicException(sprintf('The processor for the queue "%s" could not be found.', $consumer->getQueue()->getQueueName()));
-                        }
-
-                        $context = new Context($this->psrContext);
-                        $context->setLogger($logger);
-                        $context->setPsrQueue($consumer->getQueue());
-                        $context->setPsrConsumer($consumer);
-                        $context->setPsrProcessor($currentProcessor);
-                        $context->setPsrMessage($message);
-
-                        $this->doConsume($extension, $context);
-
-                        return true;
-                    });
-                } else {
-                    /** @var PsrQueue $queue */
-                    foreach ($this->boundProcessors as list($queue, $processor)) {
-                        $consumer = $consumers[$queue->getQueueName()];
-
-                        $context = new Context($this->psrContext);
-                        $context->setLogger($logger);
-                        $context->setPsrQueue($queue);
-                        $context->setPsrConsumer($consumer);
-                        $context->setPsrProcessor($processor);
-
-                        $this->doConsume($extension, $context);
-                    }
-                }
-            } catch (ConsumptionInterruptedException $e) {
-                $logger->info(sprintf('Consuming interrupted'));
+        $start = new Start(
+            $this->interopContext,
+            $this->logger,
+            $this->boundProcessors,
+            $this->receiveTimeout,
+            $startTime
+        );
 
-                $context->setExecutionInterrupted(true);
+        $extension->onStart($start);
 
-                $extension->onInterrupted($context);
+        if ($start->isExecutionInterrupted()) {
+            $this->onEnd($extension, $startTime, $start->getExitStatus());
 
-                return;
-            } catch (\Exception $exception) {
-                $context->setExecutionInterrupted(true);
-                $context->setException($exception);
+            return;
+        }
 
-                try {
-                    $this->onInterruptionByException($extension, $context);
-                } catch (\Exception $e) {
-                    // for some reason finally does not work here on php5.5
+        $this->logger = $start->getLogger();
+        $this->receiveTimeout = $start->getReceiveTimeout();
+        $this->boundProcessors = $start->getBoundProcessors();
 
-                    throw $e;
-                }
-            }
+        if (empty($this->boundProcessors)) {
+            throw new \LogicException('There is nothing to consume. It is required to bind something before calling consume method.');
         }
-    }
 
-    /**
-     * @param ExtensionInterface $extension
-     * @param Context            $context
-     *
-     * @throws ConsumptionInterruptedException
-     *
-     * @return bool
-     */
-    protected function doConsume(ExtensionInterface $extension, Context $context)
-    {
-        $processor = $context->getPsrProcessor();
-        $consumer = $context->getPsrConsumer();
-        $logger = $context->getLogger();
+        /** @var Consumer[] $consumers */
+        $consumers = [];
+        foreach ($this->boundProcessors as $queueName => $boundProcessor) {
+            $queue = $boundProcessor->getQueue();
 
-        $extension->onBeforeReceive($context);
+            $consumers[$queue->getQueueName()] = $this->interopContext->createConsumer($queue);
+        }
 
-        if ($context->isExecutionInterrupted()) {
-            throw new ConsumptionInterruptedException();
+        try {
+            $subscriptionConsumer = $this->interopContext->createSubscriptionConsumer();
+        } catch (SubscriptionConsumerNotSupportedException $e) {
+            $subscriptionConsumer = $this->fallbackSubscriptionConsumer;
         }
 
-        if ($context->getPsrMessage() || $message = $consumer->receive($this->receiveTimeout)) {
-            $logger->info('Message received from the queue: '.$context->getPsrQueue()->getQueueName());
-            $logger->debug('Headers: {headers}', ['headers' => new VarExport($message->getHeaders())]);
-            $logger->debug('Properties: {properties}', ['properties' => new VarExport($message->getProperties())]);
-            $logger->debug('Payload: {payload}', ['payload' => new VarExport($message->getBody())]);
+        $receivedMessagesCount = 0;
+        $interruptExecution = false;
+
+        $callback = function (InteropMessage $message, Consumer $consumer) use (&$receivedMessagesCount, &$interruptExecution, $extension) {
+            ++$receivedMessagesCount;
 
-            $context->setPsrMessage($message);
+            $receivedAt = (int) (microtime(true) * 1000);
+            $queue = $consumer->getQueue();
+            if (false == array_key_exists($queue->getQueueName(), $this->boundProcessors)) {
+                throw new \LogicException(sprintf('The processor for the queue "%s" could not be found.', $queue->getQueueName()));
+            }
+
+            $processor = $this->boundProcessors[$queue->getQueueName()]->getProcessor();
 
-            $extension->onPreReceived($context);
-            if (!$context->getResult()) {
-                $result = $processor->process($message, $this->psrContext);
-                $context->setResult($result);
+            $messageReceived = new MessageReceived($this->interopContext, $consumer, $message, $processor, $receivedAt, $this->logger);
+            $extension->onMessageReceived($messageReceived);
+            $result = $messageReceived->getResult();
+            $processor = $messageReceived->getProcessor();
+            if (null === $result) {
+                try {
+                    $result = $processor->process($message, $this->interopContext);
+                } catch (\Exception|\Throwable $e) {
+                    $result = $this->onProcessorException($extension, $consumer, $message, $e, $receivedAt);
+                }
             }
 
-            $extension->onResult($context);
+            $messageResult = new MessageResult($this->interopContext, $consumer, $message, $result, $receivedAt, $this->logger);
+            $extension->onResult($messageResult);
+            $result = $messageResult->getResult();
 
-            switch ($context->getResult()) {
+            switch ($result) {
                 case Result::ACK:
                     $consumer->acknowledge($message);
                     break;
@@ -284,62 +214,119 @@ protected function doConsume(ExtensionInterface $extension, Context $context)
                 case Result::REQUEUE:
                     $consumer->reject($message, true);
                     break;
+                case Result::ALREADY_ACKNOWLEDGED:
+                    break;
                 default:
-                    throw new \LogicException(sprintf('Status is not supported: %s', $context->getResult()));
+                    throw new \LogicException(sprintf('Status is not supported: %s', $result));
+            }
+
+            $postMessageReceived = new PostMessageReceived($this->interopContext, $consumer, $message, $result, $receivedAt, $this->logger);
+            $extension->onPostMessageReceived($postMessageReceived);
+
+            if ($postMessageReceived->isExecutionInterrupted()) {
+                $interruptExecution = true;
+
+                return false;
+            }
+
+            return true;
+        };
+
+        foreach ($consumers as $queueName => $consumer) {
+            /* @var Consumer $consumer */
+
+            $preSubscribe = new PreSubscribe(
+                $this->interopContext,
+                $this->boundProcessors[$queueName]->getProcessor(),
+                $consumer,
+                $this->logger
+            );
+
+            $extension->onPreSubscribe($preSubscribe);
+
+            $subscriptionConsumer->subscribe($consumer, $callback);
+        }
+
+        $cycle = 1;
+        while (true) {
+            $receivedMessagesCount = 0;
+            $interruptExecution = false;
+
+            $preConsume = new PreConsume($this->interopContext, $subscriptionConsumer, $this->logger, $cycle, $this->receiveTimeout, $startTime);
+            $extension->onPreConsume($preConsume);
+
+            if ($preConsume->isExecutionInterrupted()) {
+                $this->onEnd($extension, $startTime, $preConsume->getExitStatus(), $subscriptionConsumer);
+
+                return;
             }
 
-            $logger->info(sprintf('Message processed: %s', $context->getResult()));
+            $subscriptionConsumer->consume($this->receiveTimeout);
+
+            $postConsume = new PostConsume($this->interopContext, $subscriptionConsumer, $receivedMessagesCount, $cycle, $startTime, $this->logger);
+            $extension->onPostConsume($postConsume);
+
+            if ($interruptExecution || $postConsume->isExecutionInterrupted()) {
+                $this->onEnd($extension, $startTime, $postConsume->getExitStatus(), $subscriptionConsumer);
 
-            $extension->onPostReceived($context);
-        } else {
-            usleep($this->idleTimeout * 1000);
-            $extension->onIdle($context);
+                return;
+            }
+
+            ++$cycle;
         }
+    }
+
+    /**
+     * @internal
+     */
+    public function setFallbackSubscriptionConsumer(SubscriptionConsumer $fallbackSubscriptionConsumer): void
+    {
+        $this->fallbackSubscriptionConsumer = $fallbackSubscriptionConsumer;
+    }
+
+    private function onEnd(ExtensionInterface $extension, int $startTime, ?int $exitStatus = null, ?SubscriptionConsumer $subscriptionConsumer = null): void
+    {
+        $endTime = (int) (microtime(true) * 1000);
+
+        $endContext = new End($this->interopContext, $startTime, $endTime, $this->logger, $exitStatus);
+        $extension->onEnd($endContext);
 
-        if ($context->isExecutionInterrupted()) {
-            throw new ConsumptionInterruptedException();
+        if ($subscriptionConsumer) {
+            $subscriptionConsumer->unsubscribeAll();
         }
     }
 
     /**
-     * @param ExtensionInterface $extension
-     * @param Context            $context
+     * The logic is similar to one in Symfony's ExceptionListener::onKernelException().
      *
-     * @throws \Exception
+     * https://github.com/symfony/symfony/blob/cbe289517470eeea27162fd2d523eb29c95f775f/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php#L77
      */
-    protected function onInterruptionByException(ExtensionInterface $extension, Context $context)
+    private function onProcessorException(ExtensionInterface $extension, Consumer $consumer, Message $message, \Throwable $exception, int $receivedAt)
     {
-        $logger = $context->getLogger();
-        $logger->error(sprintf('Consuming interrupted by exception'));
-
-        $exception = $context->getException();
+        $processorException = new ProcessorException($this->interopContext, $consumer, $message, $exception, $receivedAt, $this->logger);
 
         try {
-            $extension->onInterrupted($context);
+            $extension->onProcessorException($processorException);
+
+            $result = $processorException->getResult();
+            if (null === $result) {
+                throw $exception;
+            }
+
+            return $result;
         } catch (\Exception $e) {
-            // logic is similar to one in Symfony's ExceptionListener::onKernelException
-            $logger->error(sprintf(
-                'Exception thrown when handling an exception (%s: %s at %s line %s)',
-                get_class($e),
-                $e->getMessage(),
-                $e->getFile(),
-                $e->getLine()
-            ));
-
-            $wrapper = $e;
-            while ($prev = $wrapper->getPrevious()) {
+            $prev = $e;
+            do {
                 if ($exception === $wrapper = $prev) {
                     throw $e;
                 }
-            }
+            } while ($prev = $wrapper->getPrevious());
 
-            $prev = new \ReflectionProperty('Exception', 'previous');
+            $prev = new \ReflectionProperty($wrapper instanceof \Exception ? \Exception::class : \Error::class, 'previous');
             $prev->setAccessible(true);
             $prev->setValue($wrapper, $exception);
 
             throw $e;
         }
-
-        throw $exception;
     }
 }
diff --git a/Consumption/QueueConsumerInterface.php b/Consumption/QueueConsumerInterface.php
new file mode 100644
index 0000000..ee25652
--- /dev/null
+++ b/Consumption/QueueConsumerInterface.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Interop\Queue\Context;
+use Interop\Queue\Processor;
+use Interop\Queue\Queue as InteropQueue;
+
+interface QueueConsumerInterface
+{
+    /**
+     * In milliseconds.
+     */
+    public function setReceiveTimeout(int $timeout): void;
+
+    /**
+     * In milliseconds.
+     */
+    public function getReceiveTimeout(): int;
+
+    public function getContext(): Context;
+
+    /**
+     * @param string|InteropQueue $queueName
+     */
+    public function bind($queueName, Processor $processor): self;
+
+    /**
+     * @param string|InteropQueue $queueName
+     */
+    public function bindCallback($queueName, callable $processor): self;
+
+    /**
+     * Runtime extension - is an extension or a collection of extensions which could be set on runtime.
+     * Here's a good example: @see LimitsExtensionsCommandTrait.
+     *
+     * @throws \Exception
+     */
+    public function consume(?ExtensionInterface $runtimeExtension = null): void;
+}
diff --git a/Consumption/Result.php b/Consumption/Result.php
index 6be4d51..69dd790 100644
--- a/Consumption/Result.php
+++ b/Consumption/Result.php
@@ -2,25 +2,27 @@
 
 namespace Enqueue\Consumption;
 
-use Interop\Queue\PsrMessage;
-use Interop\Queue\PsrProcessor;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Processor;
 
 class Result
 {
     /**
-     * @see PsrProcessor::ACK for more details
+     * @see Processor::ACK for more details
      */
-    const ACK = PsrProcessor::ACK;
+    public const ACK = Processor::ACK;
 
     /**
-     * @see PsrProcessor::ACK for more details
+     * @see Processor::REJECT for more details
      */
-    const REJECT = PsrProcessor::REJECT;
+    public const REJECT = Processor::REJECT;
 
     /**
-     * @see PsrProcessor::ACK for more details
+     * @see Processor::REQUEUE for more details
      */
-    const REQUEUE = PsrProcessor::REQUEUE;
+    public const REQUEUE = Processor::REQUEUE;
+
+    public const ALREADY_ACKNOWLEDGED = 'enqueue.already_acknowledged';
 
     /**
      * @var string
@@ -33,14 +35,10 @@ class Result
     private $reason;
 
     /**
-     * @var PsrMessage|null
+     * @var InteropMessage|null
      */
     private $reply;
 
-    /**
-     * @param mixed $status
-     * @param mixed $reason
-     */
     public function __construct($status, $reason = '')
     {
         $this->status = (string) $status;
@@ -72,17 +70,14 @@ public function getReason()
     }
 
     /**
-     * @return PsrMessage|null
+     * @return InteropMessage|null
      */
     public function getReply()
     {
         return $this->reply;
     }
 
-    /**
-     * @param PsrMessage|null $reply
-     */
-    public function setReply(PsrMessage $reply = null)
+    public function setReply(?InteropMessage $reply = null)
     {
         $this->reply = $reply;
     }
@@ -90,45 +85,44 @@ public function setReply(PsrMessage $reply = null)
     /**
      * @param string $reason
      *
-     * @return Result
+     * @return static
      */
     public static function ack($reason = '')
     {
-        return new static(self::ACK, $reason);
+        return new self(self::ACK, $reason);
     }
 
     /**
      * @param string $reason
      *
-     * @return Result
+     * @return static
      */
     public static function reject($reason)
     {
-        return new static(self::REJECT, $reason);
+        return new self(self::REJECT, $reason);
     }
 
     /**
      * @param string $reason
      *
-     * @return Result
+     * @return static
      */
     public static function requeue($reason = '')
     {
-        return new static(self::REQUEUE, $reason);
+        return new self(self::REQUEUE, $reason);
     }
 
     /**
-     * @param PsrMessage  $replyMessage
      * @param string      $status
      * @param string|null $reason
      *
-     * @return Result
+     * @return static
      */
-    public static function reply(PsrMessage $replyMessage, $status = self::ACK, $reason = null)
+    public static function reply(InteropMessage $replyMessage, $status = self::ACK, $reason = null)
     {
         $status = null === $status ? self::ACK : $status;
 
-        $result = new static($status, $reason);
+        $result = new self($status, $reason);
         $result->setReply($replyMessage);
 
         return $result;
diff --git a/Consumption/StartExtensionInterface.php b/Consumption/StartExtensionInterface.php
new file mode 100644
index 0000000..9857106
--- /dev/null
+++ b/Consumption/StartExtensionInterface.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Enqueue\Consumption;
+
+use Enqueue\Consumption\Context\Start;
+
+interface StartExtensionInterface
+{
+    /**
+     * Executed only once at the very beginning of the QueueConsumer::consume method call.
+     */
+    public function onStart(Start $context): void;
+}
diff --git a/Container/Container.php b/Container/Container.php
new file mode 100644
index 0000000..5abec27
--- /dev/null
+++ b/Container/Container.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Enqueue\Container;
+
+use Psr\Container\ContainerInterface;
+
+class Container implements ContainerInterface
+{
+    /**
+     * @var array
+     */
+    private $services;
+
+    public function __construct(array $services)
+    {
+        $this->services = $services;
+    }
+
+    public function get($id)
+    {
+        if (false == $this->has($id)) {
+            throw new NotFoundException(sprintf('The service "%s" not found.', $id));
+        }
+
+        return $this->services[$id];
+    }
+
+    public function has(string $id): bool
+    {
+        return array_key_exists($id, $this->services);
+    }
+}
diff --git a/Container/NotFoundException.php b/Container/NotFoundException.php
new file mode 100644
index 0000000..fcc3386
--- /dev/null
+++ b/Container/NotFoundException.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Enqueue\Container;
+
+use Psr\Container\NotFoundExceptionInterface;
+
+class NotFoundException extends \InvalidArgumentException implements NotFoundExceptionInterface
+{
+}
diff --git a/Doctrine/DoctrineConnectionFactoryFactory.php b/Doctrine/DoctrineConnectionFactoryFactory.php
new file mode 100644
index 0000000..1fd336c
--- /dev/null
+++ b/Doctrine/DoctrineConnectionFactoryFactory.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Enqueue\Doctrine;
+
+use Doctrine\Persistence\ManagerRegistry;
+use Enqueue\ConnectionFactoryFactoryInterface;
+use Enqueue\Dbal\ManagerRegistryConnectionFactory;
+use Enqueue\Dsn\Dsn;
+use Interop\Queue\ConnectionFactory;
+
+class DoctrineConnectionFactoryFactory implements ConnectionFactoryFactoryInterface
+{
+    /**
+     * @var ManagerRegistry
+     */
+    private $doctrine;
+
+    /**
+     * @var ConnectionFactoryFactoryInterface
+     */
+    private $fallbackFactory;
+
+    public function __construct(ManagerRegistry $doctrine, ConnectionFactoryFactoryInterface $fallbackFactory)
+    {
+        $this->doctrine = $doctrine;
+        $this->fallbackFactory = $fallbackFactory;
+    }
+
+    public function create($config): ConnectionFactory
+    {
+        if (is_string($config)) {
+            $config = ['dsn' => $config];
+        }
+
+        if (false == is_array($config)) {
+            throw new \InvalidArgumentException('The config must be either array or DSN string.');
+        }
+
+        if (false == array_key_exists('dsn', $config)) {
+            throw new \InvalidArgumentException('The config must have dsn key set.');
+        }
+
+        $dsn = Dsn::parseFirst($config['dsn']);
+
+        if ('doctrine' === $dsn->getScheme()) {
+            $config = $dsn->getQuery();
+            $config['connection_name'] = $dsn->getHost();
+
+            return new ManagerRegistryConnectionFactory($this->doctrine, $config);
+        }
+
+        return $this->fallbackFactory->create($config);
+    }
+}
diff --git a/Doctrine/DoctrineDriverFactory.php b/Doctrine/DoctrineDriverFactory.php
new file mode 100644
index 0000000..aab6489
--- /dev/null
+++ b/Doctrine/DoctrineDriverFactory.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Enqueue\Doctrine;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\Driver\DbalDriver;
+use Enqueue\Client\DriverFactoryInterface;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Dsn\Dsn;
+use Interop\Queue\ConnectionFactory;
+
+class DoctrineDriverFactory implements DriverFactoryInterface
+{
+    /**
+     * @var DriverFactoryInterface
+     */
+    private $fallbackFactory;
+
+    public function __construct(DriverFactoryInterface $fallbackFactory)
+    {
+        $this->fallbackFactory = $fallbackFactory;
+    }
+
+    public function create(ConnectionFactory $factory, Config $config, RouteCollection $collection): DriverInterface
+    {
+        $dsn = $config->getTransportOption('dsn');
+
+        if (empty($dsn)) {
+            throw new \LogicException('This driver factory relies on dsn option from transport config. The option is empty or not set.');
+        }
+
+        $dsn = Dsn::parseFirst($dsn);
+
+        if ('doctrine' === $dsn->getScheme()) {
+            return new DbalDriver($factory->createContext(), $config, $collection);
+        }
+
+        return $this->fallbackFactory->create($factory, $config, $collection);
+    }
+}
diff --git a/Doctrine/DoctrineSchemaCompilerPass.php b/Doctrine/DoctrineSchemaCompilerPass.php
new file mode 100644
index 0000000..0eb3784
--- /dev/null
+++ b/Doctrine/DoctrineSchemaCompilerPass.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Enqueue\Doctrine;
+
+use Enqueue\Symfony\Client\DependencyInjection\ClientFactory;
+use Enqueue\Symfony\DependencyInjection\TransportFactory;
+use Enqueue\Symfony\DiUtils;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+
+class DoctrineSchemaCompilerPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container): void
+    {
+        if (false === $container->hasDefinition('doctrine')) {
+            return;
+        }
+
+        foreach ($container->getParameter('enqueue.transports') as $name) {
+            $diUtils = DiUtils::create(TransportFactory::MODULE, $name);
+
+            $container->register($diUtils->format('connection_factory_factory.outer'), DoctrineConnectionFactoryFactory::class)
+                ->setDecoratedService($diUtils->format('connection_factory_factory'), $diUtils->format('connection_factory_factory.inner'))
+                ->addArgument(new Reference('doctrine'))
+                ->addArgument(new Reference($diUtils->format('connection_factory_factory.inner')))
+            ;
+        }
+
+        foreach ($container->getParameter('enqueue.clients') as $name) {
+            $diUtils = DiUtils::create(ClientFactory::MODULE, $name);
+
+            $container->register($diUtils->format('driver_factory.outer'), DoctrineDriverFactory::class)
+                ->setDecoratedService($diUtils->format('driver_factory'), $diUtils->format('driver_factory.inner'))
+                ->addArgument(new Reference($diUtils->format('driver_factory.inner')))
+            ;
+        }
+    }
+}
diff --git a/ProcessorRegistryInterface.php b/ProcessorRegistryInterface.php
new file mode 100644
index 0000000..5306c30
--- /dev/null
+++ b/ProcessorRegistryInterface.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Enqueue;
+
+use Interop\Queue\Processor;
+
+interface ProcessorRegistryInterface
+{
+    public function get(string $processorName): Processor;
+}
diff --git a/README.md b/README.md
index bb0b7c8..9e1dd50 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,38 @@
+<h2 align="center">Supporting Enqueue</h2>
+
+Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider:
+
+- [Become a sponsor](https://www.patreon.com/makasim)
+- [Become our client](http://forma-pro.com/)
+
+---
+
 # Message Queue.
 
 [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby)
-[![Build Status](https://travis-ci.org/php-enqueue/enqueue.png?branch=master)](https://travis-ci.org/php-enqueue/enqueue)
+[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/enqueue/ci.yml?branch=master)](https://github.com/php-enqueue/enqueue/actions?query=workflow%3ACI)
 [![Total Downloads](https://poser.pugx.org/enqueue/enqueue/d/total.png)](https://packagist.org/packages/enqueue/enqueue)
 [![Latest Stable Version](https://poser.pugx.org/enqueue/enqueue/version.png)](https://packagist.org/packages/enqueue/enqueue)
- 
-It contains advanced features build on top of a transport component. 
+
+It contains advanced features build on top of a transport component.
 Client component kind of plug and play things or consumption component that simplify message processing a lot.
-Read more about it in documentation. 
+Read more about it in documentation.
 
 ## Resources
 
-* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md)
+* [Site](https://enqueue.forma-pro.com/)
+* [Documentation](https://php-enqueue.github.io/)
 * [Questions](https://gitter.im/php-enqueue/Lobby)
 * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues)
 
 ## Developed by Forma-Pro
 
-Forma-Pro is a full stack development company which interests also spread to open source development. 
-Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. 
+Forma-Pro is a full stack development company which interests also spread to open source development.
+Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience.
 Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability.
 
 If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com
 
 ## License
 
-It is released under the [MIT License](LICENSE).
\ No newline at end of file
+It is released under the [MIT License](LICENSE).
diff --git a/Resources.php b/Resources.php
new file mode 100644
index 0000000..4c50000
--- /dev/null
+++ b/Resources.php
@@ -0,0 +1,211 @@
+<?php
+
+namespace Enqueue;
+
+use Enqueue\AmqpBunny\AmqpConnectionFactory as AmqpBunnyConnectionFactory;
+use Enqueue\AmqpExt\AmqpConnectionFactory as AmqpExtConnectionFactory;
+use Enqueue\AmqpLib\AmqpConnectionFactory as AmqpLibConnectionFactory;
+use Enqueue\Dbal\DbalConnectionFactory;
+use Enqueue\Fs\FsConnectionFactory;
+use Enqueue\Gearman\GearmanConnectionFactory;
+use Enqueue\Gps\GpsConnectionFactory;
+use Enqueue\Mongodb\MongodbConnectionFactory;
+use Enqueue\Null\NullConnectionFactory;
+use Enqueue\Pheanstalk\PheanstalkConnectionFactory;
+use Enqueue\RdKafka\RdKafkaConnectionFactory;
+use Enqueue\Redis\RedisConnectionFactory;
+use Enqueue\Sns\SnsConnectionFactory;
+use Enqueue\SnsQs\SnsQsConnectionFactory;
+use Enqueue\Sqs\SqsConnectionFactory;
+use Enqueue\Stomp\StompConnectionFactory;
+use Enqueue\Wamp\WampConnectionFactory;
+use Interop\Queue\ConnectionFactory;
+
+final class Resources
+{
+    /**
+     * [connection factory class => [
+     *   schemes => [schemes strings],
+     *   package => package name,
+     * ].
+     *
+     * @var array
+     */
+    private static $knownConnections;
+
+    private function __construct()
+    {
+    }
+
+    public static function getAvailableConnections(): array
+    {
+        $map = self::getKnownConnections();
+
+        $availableMap = [];
+        foreach ($map as $connectionClass => $item) {
+            if (\class_exists($connectionClass)) {
+                $availableMap[$connectionClass] = $item;
+            }
+        }
+
+        return $availableMap;
+    }
+
+    public static function getKnownSchemes(): array
+    {
+        $map = self::getKnownConnections();
+
+        $schemes = [];
+        foreach ($map as $connectionClass => $item) {
+            foreach ($item['schemes'] as $scheme) {
+                $schemes[$scheme] = $connectionClass;
+            }
+        }
+
+        return $schemes;
+    }
+
+    public static function getAvailableSchemes(): array
+    {
+        $map = self::getAvailableConnections();
+
+        $schemes = [];
+        foreach ($map as $connectionClass => $item) {
+            foreach ($item['schemes'] as $scheme) {
+                $schemes[$scheme] = $connectionClass;
+            }
+        }
+
+        return $schemes;
+    }
+
+    public static function getKnownConnections(): array
+    {
+        if (null === self::$knownConnections) {
+            $map = [];
+
+            $map[FsConnectionFactory::class] = [
+                'schemes' => ['file'],
+                'supportedSchemeExtensions' => [],
+                'package' => 'enqueue/fs',
+            ];
+            $map[AmqpBunnyConnectionFactory::class] = [
+                'schemes' => ['amqp'],
+                'supportedSchemeExtensions' => ['bunny'],
+                'package' => 'enqueue/amqp-bunny',
+            ];
+            $map[AmqpExtConnectionFactory::class] = [
+                'schemes' => ['amqp', 'amqps'],
+                'supportedSchemeExtensions' => ['ext'],
+                'package' => 'enqueue/amqp-ext',
+            ];
+            $map[AmqpLibConnectionFactory::class] = [
+                'schemes' => ['amqp', 'amqps'],
+                'supportedSchemeExtensions' => ['lib'],
+                'package' => 'enqueue/amqp-lib',
+            ];
+
+            $map[DbalConnectionFactory::class] = [
+                'schemes' => [
+                    'db2',
+                    'ibm-db2',
+                    'mssql',
+                    'sqlsrv',
+                    'mysql',
+                    'mysql2',
+                    'mysql',
+                    'pgsql',
+                    'postgres',
+                    'sqlite',
+                    'sqlite3',
+                    'sqlite',
+                ],
+                'supportedSchemeExtensions' => ['pdo'],
+                'package' => 'enqueue/dbal',
+            ];
+
+            $map[NullConnectionFactory::class] = [
+                'schemes' => ['null'],
+                'supportedSchemeExtensions' => [],
+                'package' => 'enqueue/null',
+            ];
+            $map[GearmanConnectionFactory::class] = [
+                'schemes' => ['gearman'],
+                'supportedSchemeExtensions' => [],
+                'package' => 'enqueue/gearman',
+            ];
+            $map[PheanstalkConnectionFactory::class] = [
+                'schemes' => ['beanstalk'],
+                'supportedSchemeExtensions' => ['pheanstalk'],
+                'package' => 'enqueue/pheanstalk',
+            ];
+            $map[RdKafkaConnectionFactory::class] = [
+                'schemes' => ['kafka', 'rdkafka'],
+                'supportedSchemeExtensions' => ['rdkafka'],
+                'package' => 'enqueue/rdkafka',
+            ];
+            $map[RedisConnectionFactory::class] = [
+                'schemes' => ['redis', 'rediss'],
+                'supportedSchemeExtensions' => ['predis', 'phpredis'],
+                'package' => 'enqueue/redis',
+            ];
+            $map[StompConnectionFactory::class] = [
+                'schemes' => ['stomp'],
+                'supportedSchemeExtensions' => ['rabbitmq'],
+                'package' => 'enqueue/stomp', ];
+            $map[SqsConnectionFactory::class] = [
+                'schemes' => ['sqs'],
+                'supportedSchemeExtensions' => [],
+                'package' => 'enqueue/sqs', ];
+            $map[SnsConnectionFactory::class] = [
+                'schemes' => ['sns'],
+                'supportedSchemeExtensions' => [],
+                'package' => 'enqueue/sns', ];
+            $map[SnsQsConnectionFactory::class] = [
+                'schemes' => ['snsqs'],
+                'supportedSchemeExtensions' => [],
+                'package' => 'enqueue/snsqs', ];
+            $map[GpsConnectionFactory::class] = [
+                'schemes' => ['gps'],
+                'supportedSchemeExtensions' => [],
+                'package' => 'enqueue/gps', ];
+            $map[MongodbConnectionFactory::class] = [
+                'schemes' => ['mongodb'],
+                'supportedSchemeExtensions' => [],
+                'package' => 'enqueue/mongodb',
+            ];
+            $map[WampConnectionFactory::class] = [
+                'schemes' => ['wamp', 'ws'],
+                'supportedSchemeExtensions' => [],
+                'package' => 'enqueue/wamp',
+            ];
+
+            self::$knownConnections = $map;
+        }
+
+        return self::$knownConnections;
+    }
+
+    public static function addConnection(string $connectionFactoryClass, array $schemes, array $extensions, string $package): void
+    {
+        if (\class_exists($connectionFactoryClass)) {
+            if (false == (new \ReflectionClass($connectionFactoryClass))->implementsInterface(ConnectionFactory::class)) {
+                throw new \InvalidArgumentException(\sprintf('The connection factory class "%s" must implement "%s" interface.', $connectionFactoryClass, ConnectionFactory::class));
+            }
+        }
+
+        if (empty($schemes)) {
+            throw new \InvalidArgumentException('Schemes could not be empty.');
+        }
+        if (empty($package)) {
+            throw new \InvalidArgumentException('Package name could not be empty.');
+        }
+
+        self::getKnownConnections();
+        self::$knownConnections[$connectionFactoryClass] = [
+            'schemes' => $schemes,
+            'supportedSchemeExtensions' => $extensions,
+            'package' => $package,
+        ];
+    }
+}
diff --git a/Router/Recipient.php b/Router/Recipient.php
index dc0ab42..d2f668f 100644
--- a/Router/Recipient.php
+++ b/Router/Recipient.php
@@ -2,33 +2,29 @@
 
 namespace Enqueue\Router;
 
-use Interop\Queue\PsrDestination;
-use Interop\Queue\PsrMessage;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as InteropMessage;
 
 class Recipient
 {
     /**
-     * @var PsrDestination
+     * @var Destination
      */
     private $destination;
 
     /**
-     * @var PsrMessage
+     * @var InteropMessage
      */
     private $message;
 
-    /**
-     * @param PsrDestination $destination
-     * @param PsrMessage     $message
-     */
-    public function __construct(PsrDestination $destination, PsrMessage $message)
+    public function __construct(Destination $destination, InteropMessage $message)
     {
         $this->destination = $destination;
         $this->message = $message;
     }
 
     /**
-     * @return PsrDestination
+     * @return Destination
      */
     public function getDestination()
     {
@@ -36,7 +32,7 @@ public function getDestination()
     }
 
     /**
-     * @return PsrMessage
+     * @return InteropMessage
      */
     public function getMessage()
     {
diff --git a/Router/RecipientListRouterInterface.php b/Router/RecipientListRouterInterface.php
index 78a354c..6bb950f 100644
--- a/Router/RecipientListRouterInterface.php
+++ b/Router/RecipientListRouterInterface.php
@@ -2,14 +2,12 @@
 
 namespace Enqueue\Router;
 
-use Interop\Queue\PsrMessage;
+use Interop\Queue\Message as InteropMessage;
 
 interface RecipientListRouterInterface
 {
     /**
-     * @param PsrMessage $message
-     *
      * @return \Traversable|Recipient[]
      */
-    public function route(PsrMessage $message);
+    public function route(InteropMessage $message);
 }
diff --git a/Router/RouteRecipientListProcessor.php b/Router/RouteRecipientListProcessor.php
index 59df355..22488e3 100644
--- a/Router/RouteRecipientListProcessor.php
+++ b/Router/RouteRecipientListProcessor.php
@@ -2,29 +2,23 @@
 
 namespace Enqueue\Router;
 
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrMessage;
-use Interop\Queue\PsrProcessor;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Processor;
 
-class RouteRecipientListProcessor implements PsrProcessor
+class RouteRecipientListProcessor implements Processor
 {
     /**
      * @var RecipientListRouterInterface
      */
     private $router;
 
-    /**
-     * @param RecipientListRouterInterface $router
-     */
     public function __construct(RecipientListRouterInterface $router)
     {
         $this->router = $router;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public function process(PsrMessage $message, PsrContext $context)
+    public function process(InteropMessage $message, Context $context)
     {
         $producer = $context->createProducer();
         foreach ($this->router->route($message) as $recipient) {
diff --git a/Rpc/Promise.php b/Rpc/Promise.php
index 91e0aaa..01b47e1 100644
--- a/Rpc/Promise.php
+++ b/Rpc/Promise.php
@@ -2,7 +2,7 @@
 
 namespace Enqueue\Rpc;
 
-use Interop\Queue\PsrMessage;
+use Interop\Queue\Message as InteropMessage;
 
 class Promise
 {
@@ -27,15 +27,10 @@ class Promise
     private $deleteReplyQueue;
 
     /**
-     * @var PsrMessage
+     * @var InteropMessage
      */
     private $message;
 
-    /**
-     * @param \Closure $receiveCallback
-     * @param \Closure $receiveNoWaitCallback
-     * @param \Closure $finallyCallback
-     */
     public function __construct(\Closure $receiveCallback, \Closure $receiveNoWaitCallback, \Closure $finallyCallback)
     {
         $this->receiveCallback = $receiveCallback;
@@ -52,7 +47,7 @@ public function __construct(\Closure $receiveCallback, \Closure $receiveNoWaitCa
      *
      * @throws TimeoutException if the wait timeout is reached
      *
-     * @return PsrMessage
+     * @return InteropMessage
      */
     public function receive($timeout = null)
     {
@@ -72,7 +67,7 @@ public function receive($timeout = null)
     /**
      * Non blocking function. Returns message or null.
      *
-     * @return PsrMessage|null
+     * @return InteropMessage|null
      */
     public function receiveNoWait()
     {
@@ -106,18 +101,16 @@ public function isDeleteReplyQueue()
     }
 
     /**
-     * @param \Closure $cb
-     * @param array    $args
+     * @param array $args
      *
-     * @return PsrMessage
+     * @return InteropMessage
      */
     private function doReceive(\Closure $cb, ...$args)
     {
         $message = call_user_func_array($cb, $args);
 
-        if (null !== $message && false == $message instanceof PsrMessage) {
-            throw new \RuntimeException(sprintf(
-                'Expected "%s" but got: "%s"', PsrMessage::class, is_object($message) ? get_class($message) : gettype($message)));
+        if (null !== $message && false == $message instanceof InteropMessage) {
+            throw new \RuntimeException(sprintf('Expected "%s" but got: "%s"', InteropMessage::class, is_object($message) ? $message::class : gettype($message)));
         }
 
         return $message;
diff --git a/Rpc/RpcClient.php b/Rpc/RpcClient.php
index d429ac9..bd3d7ce 100644
--- a/Rpc/RpcClient.php
+++ b/Rpc/RpcClient.php
@@ -3,14 +3,14 @@
 namespace Enqueue\Rpc;
 
 use Enqueue\Util\UUID;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrDestination;
-use Interop\Queue\PsrMessage;
+use Interop\Queue\Context;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as InteropMessage;
 
 class RpcClient
 {
     /**
-     * @var PsrContext
+     * @var Context
      */
     private $context;
 
@@ -19,38 +19,30 @@ class RpcClient
      */
     private $rpcFactory;
 
-    /**
-     * @param PsrContext $context
-     * @param RpcFactory $promiseFactory
-     */
-    public function __construct(PsrContext $context, RpcFactory $promiseFactory = null)
+    public function __construct(Context $context, ?RpcFactory $promiseFactory = null)
     {
         $this->context = $context;
         $this->rpcFactory = $promiseFactory ?: new RpcFactory($context);
     }
 
     /**
-     * @param PsrDestination $destination
-     * @param PsrMessage     $message
-     * @param int            $timeout
+     * @param int $timeout
      *
      * @throws TimeoutException if the wait timeout is reached
      *
-     * @return PsrMessage
+     * @return InteropMessage
      */
-    public function call(PsrDestination $destination, PsrMessage $message, $timeout)
+    public function call(Destination $destination, InteropMessage $message, $timeout)
     {
         return $this->callAsync($destination, $message, $timeout)->receive();
     }
 
     /**
-     * @param PsrDestination $destination
-     * @param PsrMessage     $message
-     * @param int            $timeout
+     * @param int $timeout
      *
      * @return Promise
      */
-    public function callAsync(PsrDestination $destination, PsrMessage $message, $timeout)
+    public function callAsync(Destination $destination, InteropMessage $message, $timeout)
     {
         if ($timeout < 1) {
             throw new \InvalidArgumentException(sprintf('Timeout must be positive not zero integer. Got %s', $timeout));
diff --git a/Rpc/RpcFactory.php b/Rpc/RpcFactory.php
index 195fd95..9100bab 100644
--- a/Rpc/RpcFactory.php
+++ b/Rpc/RpcFactory.php
@@ -2,19 +2,16 @@
 
 namespace Enqueue\Rpc;
 
-use Interop\Queue\PsrContext;
+use Interop\Queue\Context;
 
 class RpcFactory
 {
     /**
-     * @var PsrContext
+     * @var Context
      */
     private $context;
 
-    /**
-     * @param PsrContext $context
-     */
-    public function __construct(PsrContext $context)
+    public function __construct(Context $context)
     {
         $this->context = $context;
     }
diff --git a/Rpc/TimeoutException.php b/Rpc/TimeoutException.php
index a0b0655..a7f68b9 100644
--- a/Rpc/TimeoutException.php
+++ b/Rpc/TimeoutException.php
@@ -12,6 +12,6 @@ class TimeoutException extends \LogicException
      */
     public static function create($timeout, $correlationId)
     {
-        return new static(sprintf('Rpc call timeout is reached without receiving a reply message. Timeout: %s, CorrelationId: %s', $timeout, $correlationId));
+        return new self(sprintf('Rpc call timeout is reached without receiving a reply message. Timeout: %s, CorrelationId: %s', $timeout, $correlationId));
     }
 }
diff --git a/Symfony/Client/ConsumeCommand.php b/Symfony/Client/ConsumeCommand.php
new file mode 100644
index 0000000..94b56ad
--- /dev/null
+++ b/Symfony/Client/ConsumeCommand.php
@@ -0,0 +1,181 @@
+<?php
+
+namespace Enqueue\Symfony\Client;
+
+use Enqueue\Client\DriverInterface;
+use Enqueue\Consumption\ChainExtension;
+use Enqueue\Consumption\Extension\ExitStatusExtension;
+use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\QueueConsumerInterface;
+use Enqueue\Symfony\Consumption\ChooseLoggerCommandTrait;
+use Enqueue\Symfony\Consumption\LimitsExtensionsCommandTrait;
+use Enqueue\Symfony\Consumption\QueueConsumerOptionsCommandTrait;
+use Interop\Queue\Processor;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand('enqueue:consume')]
+class ConsumeCommand extends Command
+{
+    use ChooseLoggerCommandTrait;
+    use LimitsExtensionsCommandTrait;
+    use QueueConsumerOptionsCommandTrait;
+    use SetupBrokerExtensionCommandTrait;
+
+    /**
+     * @var ContainerInterface
+     */
+    private $container;
+
+    /**
+     * @var string
+     */
+    private $defaultClient;
+
+    /**
+     * @var string
+     */
+    private $queueConsumerIdPattern;
+
+    /**
+     * @var string
+     */
+    private $driverIdPattern;
+
+    /**
+     * @var string
+     */
+    private $processorIdPattern;
+
+    public function __construct(
+        ContainerInterface $container,
+        string $defaultClient,
+        string $queueConsumerIdPattern = 'enqueue.client.%s.queue_consumer',
+        string $driverIdPattern = 'enqueue.client.%s.driver',
+        string $processorIdPatter = 'enqueue.client.%s.delegate_processor',
+    ) {
+        $this->container = $container;
+        $this->defaultClient = $defaultClient;
+        $this->queueConsumerIdPattern = $queueConsumerIdPattern;
+        $this->driverIdPattern = $driverIdPattern;
+        $this->processorIdPattern = $processorIdPatter;
+
+        parent::__construct();
+    }
+
+    protected function configure(): void
+    {
+        $this->configureLimitsExtensions();
+        $this->configureSetupBrokerExtension();
+        $this->configureQueueConsumerOptions();
+        $this->configureLoggerExtension();
+
+        $this
+            ->setAliases(['enq:c'])
+            ->setDescription('A client\'s worker that processes messages. '.
+                'By default it connects to default queue. '.
+                'It select an appropriate message processor based on a message headers')
+            ->addArgument('client-queue-names', InputArgument::IS_ARRAY, 'Queues to consume messages from')
+            ->addOption('skip', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Queues to skip consumption of messages from', [])
+            ->addOption('client', 'c', InputOption::VALUE_OPTIONAL, 'The client to consume messages from.', $this->defaultClient)
+        ;
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $client = $input->getOption('client');
+
+        try {
+            $consumer = $this->getQueueConsumer($client);
+        } catch (NotFoundExceptionInterface $e) {
+            throw new \LogicException(sprintf('Client "%s" is not supported.', $client), previous: $e);
+        }
+
+        $driver = $this->getDriver($client);
+        $processor = $this->getProcessor($client);
+
+        $this->setQueueConsumerOptions($consumer, $input);
+
+        $allQueues[$driver->getConfig()->getDefaultQueue()] = true;
+        $allQueues[$driver->getConfig()->getRouterQueue()] = true;
+        foreach ($driver->getRouteCollection()->all() as $route) {
+            if (false == $route->getQueue()) {
+                continue;
+            }
+            if ($route->isProcessorExternal()) {
+                continue;
+            }
+
+            $allQueues[$route->getQueue()] = $route->isPrefixQueue();
+        }
+
+        $selectedQueues = $input->getArgument('client-queue-names');
+        if (empty($selectedQueues)) {
+            $queues = $allQueues;
+        } else {
+            $queues = [];
+            foreach ($selectedQueues as $queue) {
+                if (false == array_key_exists($queue, $allQueues)) {
+                    throw new \LogicException(sprintf('There is no such queue "%s". Available are "%s"', $queue, implode('", "', array_keys($allQueues))));
+                }
+
+                $queues[$queue] = $allQueues[$queue];
+            }
+        }
+
+        foreach ($input->getOption('skip') as $skipQueue) {
+            unset($queues[$skipQueue]);
+        }
+
+        foreach ($queues as $queue => $prefix) {
+            $queue = $driver->createQueue($queue, $prefix);
+            $consumer->bind($queue, $processor);
+        }
+
+        $runtimeExtensionChain = $this->getRuntimeExtensions($input, $output);
+        $exitStatusExtension = new ExitStatusExtension();
+
+        $consumer->consume(new ChainExtension([$runtimeExtensionChain, $exitStatusExtension]));
+
+        return $exitStatusExtension->getExitStatus() ?? 0;
+    }
+
+    protected function getRuntimeExtensions(InputInterface $input, OutputInterface $output): ExtensionInterface
+    {
+        $extensions = [];
+        $extensions = array_merge($extensions, $this->getLimitsExtensions($input, $output));
+
+        $driver = $this->getDriver($input->getOption('client'));
+
+        if ($setupBrokerExtension = $this->getSetupBrokerExtension($input, $driver)) {
+            $extensions[] = $setupBrokerExtension;
+        }
+
+        if ($loggerExtension = $this->getLoggerExtension($input, $output)) {
+            array_unshift($extensions, $loggerExtension);
+        }
+
+        return new ChainExtension($extensions);
+    }
+
+    private function getDriver(string $name): DriverInterface
+    {
+        return $this->container->get(sprintf($this->driverIdPattern, $name));
+    }
+
+    private function getQueueConsumer(string $name): QueueConsumerInterface
+    {
+        return $this->container->get(sprintf($this->queueConsumerIdPattern, $name));
+    }
+
+    private function getProcessor(string $name): Processor
+    {
+        return $this->container->get(sprintf($this->processorIdPattern, $name));
+    }
+}
diff --git a/Symfony/Client/ConsumeMessagesCommand.php b/Symfony/Client/ConsumeMessagesCommand.php
deleted file mode 100644
index 2ec98e2..0000000
--- a/Symfony/Client/ConsumeMessagesCommand.php
+++ /dev/null
@@ -1,125 +0,0 @@
-<?php
-
-namespace Enqueue\Symfony\Client;
-
-use Enqueue\Client\DelegateProcessor;
-use Enqueue\Client\DriverInterface;
-use Enqueue\Client\Meta\QueueMetaRegistry;
-use Enqueue\Consumption\ChainExtension;
-use Enqueue\Consumption\Extension\LoggerExtension;
-use Enqueue\Consumption\QueueConsumer;
-use Enqueue\Symfony\Consumption\LimitsExtensionsCommandTrait;
-use Enqueue\Symfony\Consumption\QueueConsumerOptionsCommandTrait;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputArgument;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Logger\ConsoleLogger;
-use Symfony\Component\Console\Output\OutputInterface;
-
-class ConsumeMessagesCommand extends Command
-{
-    use LimitsExtensionsCommandTrait;
-    use SetupBrokerExtensionCommandTrait;
-    use QueueConsumerOptionsCommandTrait;
-
-    /**
-     * @var QueueConsumer
-     */
-    private $consumer;
-
-    /**
-     * @var DelegateProcessor
-     */
-    private $processor;
-
-    /**
-     * @var QueueMetaRegistry
-     */
-    private $queueMetaRegistry;
-
-    /**
-     * @var DriverInterface
-     */
-    private $driver;
-
-    /**
-     * @param QueueConsumer     $consumer
-     * @param DelegateProcessor $processor
-     * @param QueueMetaRegistry $queueMetaRegistry
-     * @param DriverInterface   $driver
-     */
-    public function __construct(
-        QueueConsumer $consumer,
-        DelegateProcessor $processor,
-        QueueMetaRegistry $queueMetaRegistry,
-        DriverInterface $driver
-    ) {
-        parent::__construct(null);
-
-        $this->consumer = $consumer;
-        $this->processor = $processor;
-        $this->queueMetaRegistry = $queueMetaRegistry;
-        $this->driver = $driver;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function configure()
-    {
-        $this->configureLimitsExtensions();
-        $this->configureSetupBrokerExtension();
-        $this->configureQueueConsumerOptions();
-
-        $this
-            ->setName('enqueue:consume')
-            ->setAliases(['enq:c'])
-            ->setDescription('A client\'s worker that processes messages. '.
-                'By default it connects to default queue. '.
-                'It select an appropriate message processor based on a message headers')
-            ->addArgument('client-queue-names', InputArgument::IS_ARRAY, 'Queues to consume messages from')
-        ;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function execute(InputInterface $input, OutputInterface $output)
-    {
-        $this->setQueueConsumerOptions($this->consumer, $input);
-
-        $queueMetas = [];
-        if ($clientQueueNames = $input->getArgument('client-queue-names')) {
-            foreach ($clientQueueNames as $clientQueueName) {
-                $queueMetas[] = $this->queueMetaRegistry->getQueueMeta($clientQueueName);
-            }
-        } else {
-            $queueMetas = $this->queueMetaRegistry->getQueuesMeta();
-        }
-
-        foreach ($queueMetas as $queueMeta) {
-            $queue = $this->driver->createQueue($queueMeta->getClientName());
-            $this->consumer->bind($queue, $this->processor);
-        }
-
-        $this->consumer->consume($this->getRuntimeExtensions($input, $output));
-    }
-
-    /**
-     * @param InputInterface  $input
-     * @param OutputInterface $output
-     *
-     * @return ChainExtension
-     */
-    protected function getRuntimeExtensions(InputInterface $input, OutputInterface $output)
-    {
-        $extensions = [new LoggerExtension(new ConsoleLogger($output))];
-        $extensions = array_merge($extensions, $this->getLimitsExtensions($input, $output));
-
-        if ($setupBrokerExtension = $this->getSetupBrokerExtension($input, $this->driver)) {
-            $extensions[] = $setupBrokerExtension;
-        }
-
-        return new ChainExtension($extensions);
-    }
-}
diff --git a/Symfony/Client/ContainerAwareProcessorRegistry.php b/Symfony/Client/ContainerAwareProcessorRegistry.php
deleted file mode 100644
index 2b42cec..0000000
--- a/Symfony/Client/ContainerAwareProcessorRegistry.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-
-namespace Enqueue\Symfony\Client;
-
-use Enqueue\Client\ProcessorRegistryInterface;
-use Interop\Queue\PsrProcessor;
-use Symfony\Component\DependencyInjection\ContainerAwareInterface;
-use Symfony\Component\DependencyInjection\ContainerAwareTrait;
-
-class ContainerAwareProcessorRegistry implements ProcessorRegistryInterface, ContainerAwareInterface
-{
-    use ContainerAwareTrait;
-
-    /**
-     * @var PsrProcessor[]
-     */
-    protected $processors;
-
-    /**
-     * @param array $processors
-     */
-    public function __construct(array $processors = [])
-    {
-        $this->processors = $processors;
-    }
-
-    /**
-     * @param string $processorName
-     * @param string $serviceId
-     */
-    public function set($processorName, $serviceId)
-    {
-        $this->processors[$processorName] = $serviceId;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function get($processorName)
-    {
-        if (false == isset($this->processors[$processorName])) {
-            throw new \LogicException(sprintf('Processor was not found. processorName: "%s"', $processorName));
-        }
-
-        if (null === $this->container) {
-            throw new \LogicException('Container was not set');
-        }
-
-        $processor = $this->container->get($this->processors[$processorName]);
-
-        if (false == $processor instanceof PsrProcessor) {
-            throw new \LogicException(sprintf(
-                'Invalid instance of message processor. expected: "%s", got: "%s"',
-                PsrProcessor::class,
-                is_object($processor) ? get_class($processor) : gettype($processor)
-            ));
-        }
-
-        return $processor;
-    }
-}
diff --git a/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPass.php b/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPass.php
new file mode 100644
index 0000000..577f159
--- /dev/null
+++ b/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPass.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Enqueue\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\RouteCollection;
+use Enqueue\Symfony\DiUtils;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+final class AnalyzeRouteCollectionPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container): void
+    {
+        if (false == $container->hasParameter('enqueue.clients')) {
+            throw new \LogicException('The "enqueue.clients" parameter must be set.');
+        }
+
+        $names = $container->getParameter('enqueue.clients');
+
+        foreach ($names as $name) {
+            $diUtils = DiUtils::create(ClientFactory::MODULE, $name);
+
+            $routeCollectionId = $diUtils->format('route_collection');
+            if (false == $container->hasDefinition($routeCollectionId)) {
+                throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId));
+            }
+
+            $collection = RouteCollection::fromArray($container->getDefinition($routeCollectionId)->getArgument(0));
+
+            $this->exclusiveCommandsCouldNotBeRunOnDefaultQueue($collection);
+            $this->exclusiveCommandProcessorMustBeSingleOnGivenQueue($collection);
+            $this->customQueueNamesUnique($collection);
+            $this->defaultQueueMustBePrefixed($collection);
+        }
+    }
+
+    private function exclusiveCommandsCouldNotBeRunOnDefaultQueue(RouteCollection $collection): void
+    {
+        foreach ($collection->all() as $route) {
+            if ($route->isCommand() && $route->isProcessorExclusive() && false == $route->getQueue()) {
+                throw new \LogicException(sprintf('The command "%s" processor "%s" is exclusive but queue is not specified. Exclusive processors could not be run on a default queue.', $route->getSource(), $route->getProcessor()));
+            }
+        }
+    }
+
+    private function exclusiveCommandProcessorMustBeSingleOnGivenQueue(RouteCollection $collection): void
+    {
+        $prefixedQueues = [];
+        $queues = [];
+        foreach ($collection->all() as $route) {
+            if (false == $route->isCommand()) {
+                continue;
+            }
+            if (false == $route->isProcessorExclusive()) {
+                continue;
+            }
+
+            if ($route->isPrefixQueue()) {
+                if (array_key_exists($route->getQueue(), $prefixedQueues)) {
+                    throw new \LogicException(sprintf('The command "%s" processor "%s" is exclusive. The queue "%s" already has another exclusive command processor "%s" bound to it.', $route->getSource(), $route->getProcessor(), $route->getQueue(), $prefixedQueues[$route->getQueue()]));
+                }
+
+                $prefixedQueues[$route->getQueue()] = $route->getProcessor();
+            } else {
+                if (array_key_exists($route->getQueue(), $queues)) {
+                    throw new \LogicException(sprintf('The command "%s" processor "%s" is exclusive. The queue "%s" already has another exclusive command processor "%s" bound to it.', $route->getSource(), $route->getProcessor(), $route->getQueue(), $queues[$route->getQueue()]));
+                }
+
+                $queues[$route->getQueue()] = $route->getProcessor();
+            }
+        }
+    }
+
+    private function defaultQueueMustBePrefixed(RouteCollection $collection): void
+    {
+        foreach ($collection->all() as $route) {
+            if (false == $route->getQueue() && false == $route->isPrefixQueue()) {
+                throw new \LogicException('The default queue must be prefixed.');
+            }
+        }
+    }
+
+    private function customQueueNamesUnique(RouteCollection $collection): void
+    {
+        $prefixedQueues = [];
+        $notPrefixedQueues = [];
+
+        foreach ($collection->all() as $route) {
+            // default queue
+            $queueName = $route->getQueue();
+            if (false == $queueName) {
+                return;
+            }
+
+            $route->isPrefixQueue() ?
+                $prefixedQueues[$queueName] = $queueName :
+                $notPrefixedQueues[$queueName] = $queueName
+            ;
+        }
+
+        foreach ($notPrefixedQueues as $queueName) {
+            if (array_key_exists($queueName, $prefixedQueues)) {
+                throw new \LogicException(sprintf('There are prefixed and not prefixed queue with the same name "%s". This is not allowed.', $queueName));
+            }
+        }
+    }
+}
diff --git a/Symfony/Client/DependencyInjection/BuildClientExtensionsPass.php b/Symfony/Client/DependencyInjection/BuildClientExtensionsPass.php
new file mode 100644
index 0000000..92124f2
--- /dev/null
+++ b/Symfony/Client/DependencyInjection/BuildClientExtensionsPass.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Enqueue\Symfony\Client\DependencyInjection;
+
+use Enqueue\Symfony\DiUtils;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+
+final class BuildClientExtensionsPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container): void
+    {
+        if (false == $container->hasParameter('enqueue.clients')) {
+            throw new \LogicException('The "enqueue.clients" parameter must be set.');
+        }
+
+        $names = $container->getParameter('enqueue.clients');
+        $defaultName = $container->getParameter('enqueue.default_client');
+
+        foreach ($names as $name) {
+            $diUtils = DiUtils::create(ClientFactory::MODULE, $name);
+
+            $extensionsId = $diUtils->format('client_extensions');
+            if (false == $container->hasDefinition($extensionsId)) {
+                throw new \LogicException(sprintf('Service "%s" not found', $extensionsId));
+            }
+
+            $tags = array_merge(
+                $container->findTaggedServiceIds('enqueue.client_extension'),
+                $container->findTaggedServiceIds('enqueue.client.extension') // TODO BC
+            );
+
+            $groupByPriority = [];
+            foreach ($tags as $serviceId => $tagAttributes) {
+                foreach ($tagAttributes as $tagAttribute) {
+                    $client = $tagAttribute['client'] ?? $defaultName;
+
+                    if ($client !== $name && 'all' !== $client) {
+                        continue;
+                    }
+
+                    $priority = (int) ($tagAttribute['priority'] ?? 0);
+
+                    $groupByPriority[$priority][] = new Reference($serviceId);
+                }
+            }
+
+            krsort($groupByPriority, \SORT_NUMERIC);
+
+            $flatExtensions = [];
+            foreach ($groupByPriority as $extension) {
+                $flatExtensions = array_merge($flatExtensions, $extension);
+            }
+
+            $extensionsService = $container->getDefinition($extensionsId);
+            $extensionsService->replaceArgument(0, array_merge(
+                $extensionsService->getArgument(0),
+                $flatExtensions
+            ));
+        }
+    }
+}
diff --git a/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPass.php b/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPass.php
new file mode 100644
index 0000000..4adc09e
--- /dev/null
+++ b/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPass.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Enqueue\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\CommandSubscriberInterface;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Symfony\DiUtils;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+final class BuildCommandSubscriberRoutesPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container): void
+    {
+        if (false == $container->hasParameter('enqueue.clients')) {
+            throw new \LogicException('The "enqueue.clients" parameter must be set.');
+        }
+
+        $names = $container->getParameter('enqueue.clients');
+        $defaultName = $container->getParameter('enqueue.default_client');
+
+        foreach ($names as $name) {
+            $diUtils = DiUtils::create(ClientFactory::MODULE, $name);
+            $routeCollectionId = $diUtils->format('route_collection');
+            if (false == $container->hasDefinition($routeCollectionId)) {
+                throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId));
+            }
+
+            $tag = 'enqueue.command_subscriber';
+            $routeCollection = new RouteCollection([]);
+            foreach ($container->findTaggedServiceIds($tag) as $serviceId => $tagAttributes) {
+                $processorDefinition = $container->getDefinition($serviceId);
+                if ($processorDefinition->getFactory()) {
+                    throw new \LogicException('The command subscriber tag could not be applied to a service created by factory.');
+                }
+
+                $processorClass = $processorDefinition->getClass() ?? $serviceId;
+                if (false == class_exists($processorClass)) {
+                    throw new \LogicException(sprintf('The processor class "%s" could not be found.', $processorClass));
+                }
+
+                if (false == is_subclass_of($processorClass, CommandSubscriberInterface::class)) {
+                    throw new \LogicException(sprintf('The processor must implement "%s" interface to be used with the tag "%s"', CommandSubscriberInterface::class, $tag));
+                }
+
+                foreach ($tagAttributes as $tagAttribute) {
+                    $client = $tagAttribute['client'] ?? $defaultName;
+
+                    if ($client !== $name && 'all' !== $client) {
+                        continue;
+                    }
+
+                    /** @var CommandSubscriberInterface $processorClass */
+                    $commands = $processorClass::getSubscribedCommand();
+
+                    if (empty($commands)) {
+                        throw new \LogicException('Command subscriber must return something.');
+                    }
+
+                    if (is_string($commands)) {
+                        $commands = [$commands];
+                    }
+
+                    if (!is_array($commands)) {
+                        throw new \LogicException('Command subscriber configuration is invalid. Should be an array or string.');
+                    }
+
+                    // 0.8 command subscriber
+                    if (isset($commands['processorName'])) {
+                        @trigger_error('The command subscriber 0.8 syntax is deprecated since Enqueue 0.9.', \E_USER_DEPRECATED);
+
+                        $source = $commands['processorName'];
+                        $processor = $params['processorName'] ?? $serviceId;
+
+                        $options = $commands;
+                        unset(
+                            $options['processorName'],
+                            $options['queueName'],
+                            $options['queueNameHardcoded'],
+                            $options['exclusive'],
+                            $options['topic'],
+                            $options['source'],
+                            $options['source_type'],
+                            $options['processor'],
+                            $options['options']
+                        );
+
+                        $options['processor_service_id'] = $serviceId;
+
+                        if (isset($commands['queueName'])) {
+                            $options['queue'] = $commands['queueName'];
+                        }
+
+                        if (isset($commands['queueNameHardcoded']) && $commands['queueNameHardcoded']) {
+                            $options['prefix_queue'] = false;
+                        }
+
+                        $routeCollection->add(new Route($source, Route::COMMAND, $processor, $options));
+
+                        continue;
+                    }
+
+                    if (isset($commands['command'])) {
+                        $commands = [$commands];
+                    }
+
+                    foreach ($commands as $key => $params) {
+                        if (is_string($params)) {
+                            $routeCollection->add(new Route($params, Route::COMMAND, $serviceId, ['processor_service_id' => $serviceId]));
+                        } elseif (is_array($params)) {
+                            $source = $params['command'] ?? null;
+                            $processor = $params['processor'] ?? $serviceId;
+                            unset($params['command'], $params['source'], $params['source_type'], $params['processor'], $params['options']);
+                            $options = $params;
+                            $options['processor_service_id'] = $serviceId;
+
+                            $routeCollection->add(new Route($source, Route::COMMAND, $processor, $options));
+                        } else {
+                            throw new \LogicException(sprintf('Command subscriber configuration is invalid for "%s::getSubscribedCommand()". "%s"', $processorClass, json_encode($processorClass::getSubscribedCommand())));
+                        }
+                    }
+                }
+            }
+
+            $rawRoutes = $routeCollection->toArray();
+
+            $routeCollectionService = $container->getDefinition($routeCollectionId);
+            $routeCollectionService->replaceArgument(0, array_merge(
+                $routeCollectionService->getArgument(0),
+                $rawRoutes
+            ));
+        }
+    }
+}
diff --git a/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPass.php b/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPass.php
new file mode 100644
index 0000000..274847c
--- /dev/null
+++ b/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPass.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Enqueue\Symfony\Client\DependencyInjection;
+
+use Enqueue\Symfony\DiUtils;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+
+final class BuildConsumptionExtensionsPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container): void
+    {
+        if (false == $container->hasParameter('enqueue.clients')) {
+            throw new \LogicException('The "enqueue.clients" parameter must be set.');
+        }
+
+        $names = $container->getParameter('enqueue.clients');
+        $defaultName = $container->getParameter('enqueue.default_client');
+
+        foreach ($names as $name) {
+            $diUtils = DiUtils::create(ClientFactory::MODULE, $name);
+
+            $extensionsId = $diUtils->format('consumption_extensions');
+            if (false == $container->hasDefinition($extensionsId)) {
+                throw new \LogicException(sprintf('Service "%s" not found', $extensionsId));
+            }
+
+            $tags = array_merge(
+                $container->findTaggedServiceIds('enqueue.consumption_extension'),
+                $container->findTaggedServiceIds('enqueue.consumption.extension') // TODO BC
+            );
+
+            $groupByPriority = [];
+            foreach ($tags as $serviceId => $tagAttributes) {
+                foreach ($tagAttributes as $tagAttribute) {
+                    $client = $tagAttribute['client'] ?? $defaultName;
+
+                    if ($client !== $name && 'all' !== $client) {
+                        continue;
+                    }
+
+                    $priority = (int) ($tagAttribute['priority'] ?? 0);
+
+                    $groupByPriority[$priority][] = new Reference($serviceId);
+                }
+            }
+
+            krsort($groupByPriority, \SORT_NUMERIC);
+
+            $flatExtensions = [];
+            foreach ($groupByPriority as $extension) {
+                $flatExtensions = array_merge($flatExtensions, $extension);
+            }
+
+            $extensionsService = $container->getDefinition($extensionsId);
+            $extensionsService->replaceArgument(0, array_merge(
+                $extensionsService->getArgument(0),
+                $flatExtensions
+            ));
+        }
+    }
+}
diff --git a/Symfony/Client/DependencyInjection/BuildProcessorRegistryPass.php b/Symfony/Client/DependencyInjection/BuildProcessorRegistryPass.php
new file mode 100644
index 0000000..3759dd2
--- /dev/null
+++ b/Symfony/Client/DependencyInjection/BuildProcessorRegistryPass.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Enqueue\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\RouteCollection;
+use Enqueue\Symfony\DiUtils;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+
+final class BuildProcessorRegistryPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container): void
+    {
+        if (false == $container->hasParameter('enqueue.clients')) {
+            throw new \LogicException('The "enqueue.clients" parameter must be set.');
+        }
+
+        $names = $container->getParameter('enqueue.clients');
+
+        foreach ($names as $name) {
+            $diUtils = DiUtils::create(ClientFactory::MODULE, $name);
+
+            $processorRegistryId = $diUtils->format('processor_registry');
+            if (false == $container->hasDefinition($processorRegistryId)) {
+                throw new \LogicException(sprintf('Service "%s" not found', $processorRegistryId));
+            }
+
+            $routeCollectionId = $diUtils->format('route_collection');
+            if (false == $container->hasDefinition($routeCollectionId)) {
+                throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId));
+            }
+
+            $routerProcessorId = $diUtils->format('router_processor');
+            if (false == $container->hasDefinition($routerProcessorId)) {
+                throw new \LogicException(sprintf('Service "%s" not found', $routerProcessorId));
+            }
+
+            $routeCollection = RouteCollection::fromArray($container->getDefinition($routeCollectionId)->getArgument(0));
+
+            $map = [];
+            foreach ($routeCollection->all() as $route) {
+                if (false == $processorServiceId = $route->getOption('processor_service_id')) {
+                    throw new \LogicException('The route option "processor_service_id" is required');
+                }
+
+                $map[$route->getProcessor()] = new Reference($processorServiceId);
+            }
+
+            $map[$diUtils->parameter('router_processor')] = new Reference($routerProcessorId);
+
+            $registry = $container->getDefinition($processorRegistryId);
+            $registry->setArgument(0, ServiceLocatorTagPass::register($container, $map, $processorRegistryId));
+        }
+    }
+}
diff --git a/Symfony/Client/DependencyInjection/BuildProcessorRoutesPass.php b/Symfony/Client/DependencyInjection/BuildProcessorRoutesPass.php
new file mode 100644
index 0000000..e88cb1f
--- /dev/null
+++ b/Symfony/Client/DependencyInjection/BuildProcessorRoutesPass.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Enqueue\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Symfony\DiUtils;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+final class BuildProcessorRoutesPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container): void
+    {
+        if (false == $container->hasParameter('enqueue.clients')) {
+            throw new \LogicException('The "enqueue.clients" parameter must be set.');
+        }
+
+        $names = $container->getParameter('enqueue.clients');
+        $defaultName = $container->getParameter('enqueue.default_client');
+
+        foreach ($names as $name) {
+            $diUtils = DiUtils::create(ClientFactory::MODULE, $name);
+            $routeCollectionId = $diUtils->format('route_collection');
+            if (false == $container->hasDefinition($routeCollectionId)) {
+                throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId));
+            }
+
+            $tag = 'enqueue.processor';
+            $routeCollection = new RouteCollection([]);
+            foreach ($container->findTaggedServiceIds($tag) as $serviceId => $tagAttributes) {
+                foreach ($tagAttributes as $tagAttribute) {
+                    $client = $tagAttribute['client'] ?? $defaultName;
+
+                    if ($client !== $name && 'all' !== $client) {
+                        continue;
+                    }
+
+                    $topic = $tagAttribute['topic'] ?? null;
+                    $command = $tagAttribute['command'] ?? null;
+
+                    if (false == $topic && false == $command) {
+                        throw new \LogicException(sprintf('Either "topic" or "command" tag attribute must be set on service "%s". None is set.', $serviceId));
+                    }
+                    if ($topic && $command) {
+                        throw new \LogicException(sprintf('Either "topic" or "command" tag attribute must be set on service "%s". Both are set.', $serviceId));
+                    }
+
+                    $source = $command ?: $topic;
+                    $sourceType = $command ? Route::COMMAND : Route::TOPIC;
+                    $processor = $tagAttribute['processor'] ?? $serviceId;
+
+                    unset(
+                        $tagAttribute['topic'],
+                        $tagAttribute['command'],
+                        $tagAttribute['source'],
+                        $tagAttribute['source_type'],
+                        $tagAttribute['processor'],
+                        $tagAttribute['options']
+                    );
+                    $options = $tagAttribute;
+                    $options['processor_service_id'] = $serviceId;
+
+                    $routeCollection->add(new Route($source, $sourceType, $processor, $options));
+                }
+            }
+
+            $rawRoutes = $routeCollection->toArray();
+
+            $routeCollectionService = $container->getDefinition($routeCollectionId);
+            $routeCollectionService->replaceArgument(0, array_merge(
+                $routeCollectionService->getArgument(0),
+                $rawRoutes
+            ));
+        }
+    }
+}
diff --git a/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPass.php b/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPass.php
new file mode 100644
index 0000000..ef01e6f
--- /dev/null
+++ b/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPass.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace Enqueue\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Client\TopicSubscriberInterface;
+use Enqueue\Symfony\DiUtils;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+final class BuildTopicSubscriberRoutesPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container): void
+    {
+        if (false == $container->hasParameter('enqueue.clients')) {
+            throw new \LogicException('The "enqueue.clients" parameter must be set.');
+        }
+
+        $names = $container->getParameter('enqueue.clients');
+        $defaultName = $container->getParameter('enqueue.default_client');
+
+        foreach ($names as $name) {
+            $diUtils = DiUtils::create(ClientFactory::MODULE, $name);
+            $routeCollectionId = $diUtils->format('route_collection');
+            if (false == $container->hasDefinition($routeCollectionId)) {
+                throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId));
+            }
+
+            $tag = 'enqueue.topic_subscriber';
+            $routeCollection = new RouteCollection([]);
+            foreach ($container->findTaggedServiceIds($tag) as $serviceId => $tagAttributes) {
+                $processorDefinition = $container->getDefinition($serviceId);
+                if ($processorDefinition->getFactory()) {
+                    throw new \LogicException('The topic subscriber tag could not be applied to a service created by factory.');
+                }
+
+                $processorClass = $processorDefinition->getClass() ?? $serviceId;
+                if (false == class_exists($processorClass)) {
+                    throw new \LogicException(sprintf('The processor class "%s" could not be found.', $processorClass));
+                }
+
+                if (false == is_subclass_of($processorClass, TopicSubscriberInterface::class)) {
+                    throw new \LogicException(sprintf('The processor must implement "%s" interface to be used with the tag "%s"', TopicSubscriberInterface::class, $tag));
+                }
+
+                foreach ($tagAttributes as $tagAttribute) {
+                    $client = $tagAttribute['client'] ?? $defaultName;
+
+                    if ($client !== $name && 'all' !== $client) {
+                        continue;
+                    }
+
+                    /** @var TopicSubscriberInterface $processorClass */
+                    $topics = $processorClass::getSubscribedTopics();
+
+                    if (empty($topics)) {
+                        throw new \LogicException('Topic subscriber must return something.');
+                    }
+
+                    if (is_string($topics)) {
+                        $topics = [$topics];
+                    }
+
+                    if (!is_array($topics)) {
+                        throw new \LogicException('Topic subscriber configuration is invalid. Should be an array or string.');
+                    }
+
+                    foreach ($topics as $key => $params) {
+                        if (is_string($params)) {
+                            $routeCollection->add(new Route($params, Route::TOPIC, $serviceId, ['processor_service_id' => $serviceId]));
+
+                        // 0.8 topic subscriber
+                        } elseif (is_array($params) && is_string($key)) {
+                            @trigger_error('The topic subscriber 0.8 syntax is deprecated since Enqueue 0.9.', \E_USER_DEPRECATED);
+
+                            $source = $key;
+                            $processor = $params['processorName'] ?? $serviceId;
+
+                            $options = $params;
+                            unset(
+                                $options['processorName'],
+                                $options['queueName'],
+                                $options['queueNameHardcoded'],
+                                $options['topic'],
+                                $options['source'],
+                                $options['source_type'],
+                                $options['processor'],
+                                $options['options']
+                            );
+
+                            $options['processor_service_id'] = $serviceId;
+
+                            if (isset($params['queueName'])) {
+                                $options['queue'] = $params['queueName'];
+                            }
+
+                            if (isset($params['queueNameHardcoded']) && $params['queueNameHardcoded']) {
+                                $options['prefix_queue'] = false;
+                            }
+
+                            $routeCollection->add(new Route($source, Route::TOPIC, $processor, $options));
+                        } elseif (is_array($params)) {
+                            $source = $params['topic'] ?? null;
+                            $processor = $params['processor'] ?? $serviceId;
+                            unset($params['topic'], $params['source'], $params['source_type'], $params['processor'], $params['options']);
+                            $options = $params;
+                            $options['processor_service_id'] = $serviceId;
+
+                            $routeCollection->add(new Route($source, Route::TOPIC, $processor, $options));
+                        } else {
+                            throw new \LogicException(sprintf('Topic subscriber configuration is invalid for "%s::getSubscribedTopics()". Got "%s"', $processorClass, json_encode($processorClass::getSubscribedTopics())));
+                        }
+                    }
+                }
+            }
+
+            $rawRoutes = $routeCollection->toArray();
+
+            $routeCollectionService = $container->getDefinition($routeCollectionId);
+            $routeCollectionService->replaceArgument(0, array_merge(
+                $routeCollectionService->getArgument(0),
+                $rawRoutes
+            ));
+        }
+    }
+}
diff --git a/Symfony/Client/DependencyInjection/ClientFactory.php b/Symfony/Client/DependencyInjection/ClientFactory.php
new file mode 100644
index 0000000..be020dc
--- /dev/null
+++ b/Symfony/Client/DependencyInjection/ClientFactory.php
@@ -0,0 +1,254 @@
+<?php
+
+namespace Enqueue\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\ChainExtension;
+use Enqueue\Client\Config;
+use Enqueue\Client\ConsumptionExtension\DelayRedeliveredMessageExtension;
+use Enqueue\Client\ConsumptionExtension\ExclusiveCommandExtension;
+use Enqueue\Client\ConsumptionExtension\FlushSpoolProducerExtension;
+use Enqueue\Client\ConsumptionExtension\SetRouterPropertiesExtension;
+use Enqueue\Client\DelegateProcessor;
+use Enqueue\Client\DriverFactory;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Producer;
+use Enqueue\Client\ProducerInterface;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Client\RouterProcessor;
+use Enqueue\Client\SpoolProducer;
+use Enqueue\Client\TraceableProducer;
+use Enqueue\Consumption\ChainExtension as ConsumptionChainExtension;
+use Enqueue\Consumption\QueueConsumer;
+use Enqueue\Rpc\RpcFactory;
+use Enqueue\Symfony\Client\FlushSpoolProducerListener;
+use Enqueue\Symfony\Client\LazyProducer;
+use Enqueue\Symfony\ContainerProcessorRegistry;
+use Enqueue\Symfony\DependencyInjection\TransportFactory;
+use Enqueue\Symfony\DiUtils;
+use Interop\Queue\Context;
+use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
+use Symfony\Component\Config\Definition\Builder\NodeDefinition;
+use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * @internal
+ */
+final class ClientFactory
+{
+    public const MODULE = 'client';
+
+    /**
+     * @var bool
+     */
+    private $default;
+
+    /**
+     * @var DiUtils
+     */
+    private $diUtils;
+
+    public function __construct(string $name, bool $default = false)
+    {
+        if (empty($name)) {
+            throw new \InvalidArgumentException('The name could not be empty.');
+        }
+
+        $this->default = $default;
+        $this->diUtils = DiUtils::create(self::MODULE, $name);
+    }
+
+    public static function getConfiguration(bool $debug, string $name = 'client'): NodeDefinition
+    {
+        $builder = new ArrayNodeDefinition($name);
+
+        $builder->children()
+            ->booleanNode('traceable_producer')->defaultValue($debug)->end()
+            ->scalarNode('prefix')->defaultValue('enqueue')->end()
+            ->scalarNode('separator')->defaultValue('.')->end()
+            ->scalarNode('app_name')->defaultValue('app')->end()
+            ->scalarNode('router_topic')->defaultValue('default')->cannotBeEmpty()->end()
+            ->scalarNode('router_queue')->defaultValue('default')->cannotBeEmpty()->end()
+            ->scalarNode('router_processor')->defaultNull()->end()
+            ->integerNode('redelivered_delay_time')->min(0)->defaultValue(0)->end()
+            ->scalarNode('default_queue')->defaultValue('default')->cannotBeEmpty()->end()
+            ->arrayNode('driver_options')->addDefaultsIfNotSet()->info('The array contains driver specific options')->ignoreExtraKeys(false)->end()
+            ->end()
+        ;
+
+        return $builder;
+    }
+
+    public function build(ContainerBuilder $container, array $config): void
+    {
+        $container->register($this->diUtils->format('context'), Context::class)
+            ->setFactory([$this->diUtils->reference('driver'), 'getContext'])
+        ;
+
+        $container->register($this->diUtils->format('driver_factory'), DriverFactory::class);
+
+        $routerProcessor = empty($config['router_processor'])
+            ? $this->diUtils->format('router_processor')
+            : $config['router_processor']
+        ;
+
+        $container->register($this->diUtils->format('config'), Config::class)
+            ->setArguments([
+                $config['prefix'],
+                $config['separator'],
+                $config['app_name'],
+                $config['router_topic'],
+                $config['router_queue'],
+                $config['default_queue'],
+                $routerProcessor,
+                $config['transport'],
+                $config['driver_options'] ?? [],
+            ]);
+
+        $container->setParameter($this->diUtils->format('router_processor'), $routerProcessor);
+        $container->setParameter($this->diUtils->format('router_queue_name'), $config['router_queue']);
+        $container->setParameter($this->diUtils->format('default_queue_name'), $config['default_queue']);
+
+        $container->register($this->diUtils->format('route_collection'), RouteCollection::class)
+            ->addArgument([])
+            ->setFactory([RouteCollection::class, 'fromArray'])
+        ;
+
+        $container->register($this->diUtils->format('producer'), Producer::class)
+            // @deprecated
+            ->setPublic(true)
+            ->addArgument($this->diUtils->reference('driver'))
+            ->addArgument($this->diUtils->reference('rpc_factory'))
+            ->addArgument($this->diUtils->reference('client_extensions'))
+        ;
+
+        $lazyProducer = $container->register($this->diUtils->format('lazy_producer'), LazyProducer::class);
+        $lazyProducer->addArgument(ServiceLocatorTagPass::register($container, [
+            $this->diUtils->format('producer') => new Reference($this->diUtils->format('producer')),
+        ]));
+        $lazyProducer->addArgument($this->diUtils->format('producer'));
+
+        $container->register($this->diUtils->format('spool_producer'), SpoolProducer::class)
+            ->addArgument($this->diUtils->reference('lazy_producer'))
+        ;
+
+        $container->register($this->diUtils->format('client_extensions'), ChainExtension::class)
+            ->addArgument([])
+        ;
+
+        $container->register($this->diUtils->format('rpc_factory'), RpcFactory::class)
+            ->addArgument($this->diUtils->reference('context'))
+        ;
+
+        $container->register($this->diUtils->format('router_processor'), RouterProcessor::class)
+            ->addArgument($this->diUtils->reference('driver'))
+        ;
+
+        $container->register($this->diUtils->format('processor_registry'), ContainerProcessorRegistry::class);
+
+        $container->register($this->diUtils->format('delegate_processor'), DelegateProcessor::class)
+            ->addArgument($this->diUtils->reference('processor_registry'))
+        ;
+
+        $container->register($this->diUtils->format('set_router_properties_extension'), SetRouterPropertiesExtension::class)
+            ->addArgument($this->diUtils->reference('driver'))
+            ->addTag('enqueue.consumption_extension', ['priority' => 100, 'client' => $this->diUtils->getConfigName()])
+        ;
+
+        $container->register($this->diUtils->format('queue_consumer'), QueueConsumer::class)
+            ->addArgument($this->diUtils->reference('context'))
+            ->addArgument($this->diUtils->reference('consumption_extensions'))
+            ->addArgument([])
+            ->addArgument(new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE))
+            ->addArgument($config['consumption']['receive_timeout'])
+        ;
+
+        $container->register($this->diUtils->format('consumption_extensions'), ConsumptionChainExtension::class)
+            ->addArgument([])
+        ;
+
+        $container->register($this->diUtils->format('flush_spool_producer_extension'), FlushSpoolProducerExtension::class)
+            ->addArgument($this->diUtils->reference('spool_producer'))
+            ->addTag('enqueue.consumption.extension', ['priority' => -100, 'client' => $this->diUtils->getConfigName()])
+        ;
+
+        $container->register($this->diUtils->format('exclusive_command_extension'), ExclusiveCommandExtension::class)
+            ->addArgument($this->diUtils->reference('driver'))
+            ->addTag('enqueue.consumption.extension', ['priority' => 100, 'client' => $this->diUtils->getConfigName()])
+        ;
+
+        if ($config['traceable_producer']) {
+            $container->register($this->diUtils->format('traceable_producer'), TraceableProducer::class)
+                ->setDecoratedService($this->diUtils->format('producer'))
+                ->addArgument($this->diUtils->reference('traceable_producer.inner'))
+            ;
+        }
+
+        if ($config['redelivered_delay_time']) {
+            $container->register($this->diUtils->format('delay_redelivered_message_extension'), DelayRedeliveredMessageExtension::class)
+                ->addArgument($this->diUtils->reference('driver'))
+                ->addArgument($config['redelivered_delay_time'])
+                ->addTag('enqueue.consumption_extension', ['priority' => 10, 'client' => $this->diUtils->getConfigName()])
+            ;
+
+            $container->getDefinition($this->diUtils->format('delay_redelivered_message_extension'))
+                ->replaceArgument(1, $config['redelivered_delay_time'])
+            ;
+        }
+
+        $locatorId = 'enqueue.locator';
+        if ($container->hasDefinition($locatorId)) {
+            $locator = $container->getDefinition($locatorId);
+            $locator->replaceArgument(0, array_replace($locator->getArgument(0), [
+                $this->diUtils->format('queue_consumer') => $this->diUtils->reference('queue_consumer'),
+                $this->diUtils->format('driver') => $this->diUtils->reference('driver'),
+                $this->diUtils->format('delegate_processor') => $this->diUtils->reference('delegate_processor'),
+                $this->diUtils->format('producer') => $this->diUtils->reference('lazy_producer'),
+            ]));
+        }
+
+        if ($this->default) {
+            $container->setAlias(ProducerInterface::class, $this->diUtils->format('lazy_producer'));
+            $container->setAlias(SpoolProducer::class, $this->diUtils->format('spool_producer'));
+
+            if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) {
+                $container->setAlias($this->diUtils->formatDefault('producer'), $this->diUtils->format('producer'));
+                $container->setAlias($this->diUtils->formatDefault('spool_producer'), $this->diUtils->format('spool_producer'));
+            }
+        }
+    }
+
+    public function createDriver(ContainerBuilder $container, array $config): string
+    {
+        $factoryId = DiUtils::create(TransportFactory::MODULE, $this->diUtils->getConfigName())->format('connection_factory');
+        $driverId = $this->diUtils->format('driver');
+        $driverFactoryId = $this->diUtils->format('driver_factory');
+
+        $container->register($driverId, DriverInterface::class)
+            ->setFactory([new Reference($driverFactoryId), 'create'])
+            ->addArgument(new Reference($factoryId))
+            ->addArgument($this->diUtils->reference('config'))
+            ->addArgument($this->diUtils->reference('route_collection'))
+        ;
+
+        if ($this->default) {
+            $container->setAlias(DriverInterface::class, $driverId);
+
+            if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) {
+                $container->setAlias($this->diUtils->formatDefault('driver'), $driverId);
+            }
+        }
+
+        return $driverId;
+    }
+
+    public function createFlushSpoolProducerListener(ContainerBuilder $container): void
+    {
+        $container->register($this->diUtils->format('flush_spool_producer_listener'), FlushSpoolProducerListener::class)
+            ->addArgument($this->diUtils->reference('spool_producer'))
+            ->addTag('kernel.event_subscriber')
+        ;
+    }
+}
diff --git a/Symfony/Client/FlushSpoolProducerListener.php b/Symfony/Client/FlushSpoolProducerListener.php
index 1f5fdcb..00543f6 100644
--- a/Symfony/Client/FlushSpoolProducerListener.php
+++ b/Symfony/Client/FlushSpoolProducerListener.php
@@ -14,9 +14,6 @@ class FlushSpoolProducerListener implements EventSubscriberInterface
      */
     private $producer;
 
-    /**
-     * @param SpoolProducer $producer
-     */
     public function __construct(SpoolProducer $producer)
     {
         $this->producer = $producer;
@@ -27,10 +24,7 @@ public function flushMessages()
         $this->producer->flush();
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    public static function getSubscribedEvents()
+    public static function getSubscribedEvents(): array
     {
         $events = [];
 
diff --git a/Symfony/Client/LazyProducer.php b/Symfony/Client/LazyProducer.php
new file mode 100644
index 0000000..8dd3aad
--- /dev/null
+++ b/Symfony/Client/LazyProducer.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Enqueue\Symfony\Client;
+
+use Enqueue\Client\ProducerInterface;
+use Enqueue\Rpc\Promise;
+use Psr\Container\ContainerInterface;
+
+class LazyProducer implements ProducerInterface
+{
+    private $container;
+
+    private $producerId;
+
+    public function __construct(ContainerInterface $container, string $producerId)
+    {
+        $this->container = $container;
+        $this->producerId = $producerId;
+    }
+
+    public function sendEvent(string $topic, $message): void
+    {
+        $this->getRealProducer()->sendEvent($topic, $message);
+    }
+
+    public function sendCommand(string $command, $message, bool $needReply = false): ?Promise
+    {
+        return $this->getRealProducer()->sendCommand($command, $message, $needReply);
+    }
+
+    private function getRealProducer(): ProducerInterface
+    {
+        return $this->container->get($this->producerId);
+    }
+}
diff --git a/Symfony/Client/Meta/QueuesCommand.php b/Symfony/Client/Meta/QueuesCommand.php
deleted file mode 100644
index 02d263d..0000000
--- a/Symfony/Client/Meta/QueuesCommand.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-namespace Enqueue\Symfony\Client\Meta;
-
-use Enqueue\Client\Meta\QueueMetaRegistry;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Helper\Table;
-use Symfony\Component\Console\Helper\TableSeparator;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-
-class QueuesCommand extends Command
-{
-    /**
-     * @var QueueMetaRegistry
-     */
-    private $queueMetaRegistry;
-
-    /**
-     * @param QueueMetaRegistry $queueRegistry
-     */
-    public function __construct(QueueMetaRegistry $queueRegistry)
-    {
-        parent::__construct(null);
-
-        $this->queueMetaRegistry = $queueRegistry;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function configure()
-    {
-        $this
-            ->setName('enqueue:queues')
-            ->setAliases([
-                'enq:m:q',
-                'debug:enqueue:queues',
-            ])
-            ->setDescription('A command shows all available queues and some information about them.')
-        ;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function execute(InputInterface $input, OutputInterface $output)
-    {
-        $table = new Table($output);
-        $table->setHeaders(['Client Name', 'Transport Name', 'processors']);
-
-        $count = 0;
-        $firstRow = true;
-        foreach ($this->queueMetaRegistry->getQueuesMeta() as $queueMeta) {
-            if (false == $firstRow) {
-                $table->addRow(new TableSeparator());
-            }
-
-            $table->addRow([
-                $queueMeta->getClientName(),
-                $queueMeta->getClientName() == $queueMeta->getTransportName() ? '(same)' : $queueMeta->getTransportName(),
-                implode(PHP_EOL, $queueMeta->getProcessors()),
-            ]);
-
-            ++$count;
-            $firstRow = false;
-        }
-
-        $output->writeln(sprintf('Found %s destinations', $count));
-        $output->writeln('');
-        $table->render();
-    }
-}
diff --git a/Symfony/Client/Meta/TopicsCommand.php b/Symfony/Client/Meta/TopicsCommand.php
deleted file mode 100644
index 8314fb9..0000000
--- a/Symfony/Client/Meta/TopicsCommand.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-namespace Enqueue\Symfony\Client\Meta;
-
-use Enqueue\Client\Meta\TopicMetaRegistry;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Helper\Table;
-use Symfony\Component\Console\Helper\TableSeparator;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-
-class TopicsCommand extends Command
-{
-    /**
-     * @var TopicMetaRegistry
-     */
-    private $topicRegistry;
-
-    /**
-     * @param TopicMetaRegistry $topicRegistry
-     */
-    public function __construct(TopicMetaRegistry $topicRegistry)
-    {
-        parent::__construct(null);
-
-        $this->topicRegistry = $topicRegistry;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function configure()
-    {
-        $this
-            ->setName('enqueue:topics')
-            ->setAliases([
-                'enq:m:t',
-                'debug:enqueue:topics',
-            ])
-            ->setDescription('A command shows all available topics and some information about them.')
-        ;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function execute(InputInterface $input, OutputInterface $output)
-    {
-        $table = new Table($output);
-        $table->setHeaders(['Topic', 'Description', 'processors']);
-
-        $count = 0;
-        $firstRow = true;
-        foreach ($this->topicRegistry->getTopicsMeta() as $topic) {
-            if (false == $firstRow) {
-                $table->addRow(new TableSeparator());
-            }
-
-            $table->addRow([$topic->getName(), $topic->getDescription(), implode(PHP_EOL, $topic->getProcessors())]);
-
-            ++$count;
-            $firstRow = false;
-        }
-
-        $output->writeln(sprintf('Found %s topics', $count));
-        $output->writeln('');
-        $table->render();
-    }
-}
diff --git a/Symfony/Client/ProduceCommand.php b/Symfony/Client/ProduceCommand.php
new file mode 100644
index 0000000..953a766
--- /dev/null
+++ b/Symfony/Client/ProduceCommand.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Enqueue\Symfony\Client;
+
+use Enqueue\Client\Message;
+use Enqueue\Client\ProducerInterface;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand('enqueue:produce')]
+class ProduceCommand extends Command
+{
+    /**
+     * @var ContainerInterface
+     */
+    private $container;
+
+    /**
+     * @var string
+     */
+    private $defaultClient;
+
+    /**
+     * @var string
+     */
+    private $producerIdPattern;
+
+    public function __construct(ContainerInterface $container, string $defaultClient, string $producerIdPattern = 'enqueue.client.%s.producer')
+    {
+        $this->container = $container;
+        $this->defaultClient = $defaultClient;
+        $this->producerIdPattern = $producerIdPattern;
+
+        parent::__construct();
+    }
+
+    protected function configure(): void
+    {
+        $this
+            ->setDescription('Sends an event to the topic')
+            ->addArgument('message', InputArgument::REQUIRED, 'A message')
+            ->addOption('header', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'The message headers')
+            ->addOption('client', 'c', InputOption::VALUE_OPTIONAL, 'The client to consume messages from.', $this->defaultClient)
+            ->addOption('topic', null, InputOption::VALUE_OPTIONAL, 'The topic to send a message to')
+            ->addOption('command', null, InputOption::VALUE_OPTIONAL, 'The command to send a message to')
+        ;
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $topic = $input->getOption('topic');
+        $command = $input->getOption('command');
+        $message = $input->getArgument('message');
+        $headers = (array) $input->getOption('header');
+        $client = $input->getOption('client');
+
+        if ($topic && $command) {
+            throw new \LogicException('Either topic or command option should be set, both are set.');
+        }
+
+        try {
+            $producer = $this->getProducer($client);
+        } catch (NotFoundExceptionInterface $e) {
+            throw new \LogicException(sprintf('Client "%s" is not supported.', $client), previous: $e);
+        }
+
+        if ($topic) {
+            $producer->sendEvent($topic, new Message($message, [], $headers));
+
+            $output->writeln('An event is sent');
+        } elseif ($command) {
+            $producer->sendCommand($command, $message);
+
+            $output->writeln('A command is sent');
+        } else {
+            throw new \LogicException('Either topic or command option should be set, none is set.');
+        }
+
+        return 0;
+    }
+
+    private function getProducer(string $client): ProducerInterface
+    {
+        return $this->container->get(sprintf($this->producerIdPattern, $client));
+    }
+}
diff --git a/Symfony/Client/ProduceMessageCommand.php b/Symfony/Client/ProduceMessageCommand.php
deleted file mode 100644
index a9f9ef9..0000000
--- a/Symfony/Client/ProduceMessageCommand.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-namespace Enqueue\Symfony\Client;
-
-use Enqueue\Client\ProducerInterface;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputArgument;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-
-class ProduceMessageCommand extends Command
-{
-    /**
-     * @var ProducerInterface
-     */
-    private $producer;
-
-    /**
-     * @param ProducerInterface $producer
-     */
-    public function __construct(ProducerInterface $producer)
-    {
-        parent::__construct(null);
-
-        $this->producer = $producer;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function configure()
-    {
-        $this
-            ->setName('enqueue:produce')
-            ->setAliases(['enq:p'])
-            ->setDescription('A command to send a message to topic')
-            ->addArgument('topic', InputArgument::REQUIRED, 'A topic to send message to')
-            ->addArgument('message', InputArgument::REQUIRED, 'A message to send')
-        ;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function execute(InputInterface $input, OutputInterface $output)
-    {
-        $this->producer->sendEvent(
-            $input->getArgument('topic'),
-            $input->getArgument('message')
-        );
-
-        $output->writeln('Message is sent');
-    }
-}
diff --git a/Symfony/Client/RoutesCommand.php b/Symfony/Client/RoutesCommand.php
new file mode 100644
index 0000000..04b657e
--- /dev/null
+++ b/Symfony/Client/RoutesCommand.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Enqueue\Symfony\Client;
+
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Route;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Helper\TableSeparator;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand('enqueue:routes')]
+class RoutesCommand extends Command
+{
+    /**
+     * @var ContainerInterface
+     */
+    private $container;
+
+    /**
+     * @var string
+     */
+    private $defaultClient;
+
+    /**
+     * @var string
+     */
+    private $driverIdPatter;
+
+    /**
+     * @var DriverInterface
+     */
+    private $driver;
+
+    public function __construct(ContainerInterface $container, string $defaultClient, string $driverIdPatter = 'enqueue.client.%s.driver')
+    {
+        $this->container = $container;
+        $this->defaultClient = $defaultClient;
+        $this->driverIdPatter = $driverIdPatter;
+
+        parent::__construct();
+    }
+
+    protected function configure(): void
+    {
+        $this
+            ->setAliases(['debug:enqueue:routes'])
+            ->setDescription('A command lists all registered routes.')
+            ->addOption('show-route-options', null, InputOption::VALUE_NONE, 'Adds ability to hide options.')
+            ->addOption('client', 'c', InputOption::VALUE_OPTIONAL, 'The client to consume messages from.', $this->defaultClient)
+        ;
+
+        $this->driver = null;
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        try {
+            $this->driver = $this->getDriver($input->getOption('client'));
+        } catch (NotFoundExceptionInterface $e) {
+            throw new \LogicException(sprintf('Client "%s" is not supported.', $input->getOption('client')), previous: $e);
+        }
+
+        $routes = $this->driver->getRouteCollection()->all();
+        $output->writeln(sprintf('Found %s routes', count($routes)));
+        $output->writeln('');
+
+        if ($routes) {
+            $table = new Table($output);
+            $table->setHeaders(['Type', 'Source', 'Queue', 'Processor', 'Options']);
+
+            $firstRow = true;
+            foreach ($routes as $route) {
+                if (false == $firstRow) {
+                    $table->addRow(new TableSeparator());
+
+                    $firstRow = false;
+                }
+
+                if ($route->isCommand()) {
+                    continue;
+                }
+
+                $table->addRow([
+                    $this->formatSourceType($route),
+                    $route->getSource(),
+                    $this->formatQueue($route),
+                    $this->formatProcessor($route),
+                    $input->getOption('show-route-options') ? $this->formatOptions($route) : '(hidden)',
+                ]);
+            }
+
+            foreach ($routes as $route) {
+                if ($route->isTopic()) {
+                    continue;
+                }
+
+                $table->addRow([
+                    $this->formatSourceType($route),
+                    $route->getSource(),
+                    $this->formatQueue($route),
+                    $this->formatProcessor($route),
+                    $input->getOption('show-route-options') ? $this->formatOptions($route) : '(hidden)',
+                ]);
+            }
+
+            $table->render();
+        }
+
+        return 0;
+    }
+
+    private function formatSourceType(Route $route): string
+    {
+        if ($route->isCommand()) {
+            return 'command';
+        }
+
+        if ($route->isTopic()) {
+            return 'topic';
+        }
+
+        return 'unknown';
+    }
+
+    private function formatProcessor(Route $route): string
+    {
+        if ($route->isProcessorExternal()) {
+            return 'n\a (external)';
+        }
+
+        $processor = $route->getProcessor();
+
+        return $route->isProcessorExclusive() ? $processor.' (exclusive)' : $processor;
+    }
+
+    private function formatQueue(Route $route): string
+    {
+        $queue = $route->getQueue() ?: $this->driver->getConfig()->getDefaultQueue();
+
+        return $route->isPrefixQueue() ? $queue.' (prefixed)' : $queue.' (as is)';
+    }
+
+    private function formatOptions(Route $route): string
+    {
+        return var_export($route->getOptions(), true);
+    }
+
+    private function getDriver(string $client): DriverInterface
+    {
+        return $this->container->get(sprintf($this->driverIdPatter, $client));
+    }
+}
+
+function enqueue()
+{
+}
diff --git a/Symfony/Client/SetupBrokerCommand.php b/Symfony/Client/SetupBrokerCommand.php
index c825bd2..92d5ad0 100644
--- a/Symfony/Client/SetupBrokerCommand.php
+++ b/Symfony/Client/SetupBrokerCommand.php
@@ -3,47 +3,68 @@
 namespace Enqueue\Symfony\Client;
 
 use Enqueue\Client\DriverInterface;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Logger\ConsoleLogger;
 use Symfony\Component\Console\Output\OutputInterface;
 
+#[AsCommand('enqueue:setup-broker')]
 class SetupBrokerCommand extends Command
 {
     /**
-     * @var DriverInterface
+     * @var ContainerInterface
      */
-    private $driver;
+    private $container;
 
     /**
-     * @param DriverInterface $driver
+     * @var string
      */
-    public function __construct(DriverInterface $driver)
+    private $defaultClient;
+
+    /**
+     * @var string
+     */
+    private $driverIdPattern;
+
+    public function __construct(ContainerInterface $container, string $defaultClient, string $driverIdPattern = 'enqueue.client.%s.driver')
     {
-        parent::__construct(null);
+        $this->container = $container;
+        $this->defaultClient = $defaultClient;
+        $this->driverIdPattern = $driverIdPattern;
 
-        $this->driver = $driver;
+        parent::__construct();
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    protected function configure()
+    protected function configure(): void
     {
         $this
-            ->setName('enqueue:setup-broker')
             ->setAliases(['enq:sb'])
-            ->setDescription('Creates all required queues')
+            ->setDescription('Setup broker. Configure the broker, creates queues, topics and so on.')
+            ->addOption('client', 'c', InputOption::VALUE_OPTIONAL, 'The client to consume messages from.', $this->defaultClient)
         ;
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    protected function execute(InputInterface $input, OutputInterface $output)
+    protected function execute(InputInterface $input, OutputInterface $output): int
     {
-        $output->writeln('Setup Broker');
+        $client = $input->getOption('client');
+
+        try {
+            $this->getDriver($client)->setupBroker(new ConsoleLogger($output));
+        } catch (NotFoundExceptionInterface $e) {
+            throw new \LogicException(sprintf('Client "%s" is not supported.', $client), previous: $e);
+        }
+
+        $output->writeln('Broker set up');
 
-        $this->driver->setupBroker(new ConsoleLogger($output));
+        return 0;
+    }
+
+    private function getDriver(string $client): DriverInterface
+    {
+        return $this->container->get(sprintf($this->driverIdPattern, $client));
     }
 }
diff --git a/Symfony/Client/SetupBrokerExtensionCommandTrait.php b/Symfony/Client/SetupBrokerExtensionCommandTrait.php
index 2888f56..bcc4f7b 100644
--- a/Symfony/Client/SetupBrokerExtensionCommandTrait.php
+++ b/Symfony/Client/SetupBrokerExtensionCommandTrait.php
@@ -10,9 +10,6 @@
 
 trait SetupBrokerExtensionCommandTrait
 {
-    /**
-     * {@inheritdoc}
-     */
     protected function configureSetupBrokerExtension()
     {
         $this
@@ -21,10 +18,7 @@ protected function configureSetupBrokerExtension()
     }
 
     /**
-     * @param InputInterface  $input
-     * @param DriverInterface $driver
-     *
-     * @return ExtensionInterface
+     * @return ExtensionInterface|null
      */
     protected function getSetupBrokerExtension(InputInterface $input, DriverInterface $driver)
     {
diff --git a/Symfony/Client/SimpleConsumeCommand.php b/Symfony/Client/SimpleConsumeCommand.php
new file mode 100644
index 0000000..fafc35d
--- /dev/null
+++ b/Symfony/Client/SimpleConsumeCommand.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Enqueue\Symfony\Client;
+
+use Enqueue\Client\DriverInterface;
+use Enqueue\Consumption\QueueConsumerInterface;
+use Enqueue\Container\Container;
+use Interop\Queue\Processor;
+
+class SimpleConsumeCommand extends ConsumeCommand
+{
+    public function __construct(QueueConsumerInterface $queueConsumer, DriverInterface $driver, Processor $processor)
+    {
+        parent::__construct(
+            new Container([
+                'queue_consumer' => $queueConsumer,
+                'driver' => $driver,
+                'processor' => $processor,
+            ]),
+            'default',
+            'queue_consumer',
+            'driver',
+            'processor'
+        );
+    }
+}
diff --git a/Symfony/Client/SimpleProduceCommand.php b/Symfony/Client/SimpleProduceCommand.php
new file mode 100644
index 0000000..5d7f765
--- /dev/null
+++ b/Symfony/Client/SimpleProduceCommand.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Enqueue\Symfony\Client;
+
+use Enqueue\Client\ProducerInterface;
+use Enqueue\Container\Container;
+
+class SimpleProduceCommand extends ProduceCommand
+{
+    public function __construct(ProducerInterface $producer)
+    {
+        parent::__construct(
+            new Container(['producer' => $producer]),
+            'default',
+            'producer'
+        );
+    }
+}
diff --git a/Symfony/Client/SimpleRoutesCommand.php b/Symfony/Client/SimpleRoutesCommand.php
new file mode 100644
index 0000000..0023f14
--- /dev/null
+++ b/Symfony/Client/SimpleRoutesCommand.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Enqueue\Symfony\Client;
+
+use Enqueue\Client\DriverInterface;
+use Enqueue\Container\Container;
+
+class SimpleRoutesCommand extends RoutesCommand
+{
+    public function __construct(DriverInterface $driver)
+    {
+        parent::__construct(
+            new Container(['driver' => $driver]),
+            'default',
+            'driver'
+        );
+    }
+}
diff --git a/Symfony/Client/SimpleSetupBrokerCommand.php b/Symfony/Client/SimpleSetupBrokerCommand.php
new file mode 100644
index 0000000..aae19f8
--- /dev/null
+++ b/Symfony/Client/SimpleSetupBrokerCommand.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Enqueue\Symfony\Client;
+
+use Enqueue\Client\DriverInterface;
+use Enqueue\Container\Container;
+
+class SimpleSetupBrokerCommand extends SetupBrokerCommand
+{
+    public function __construct(DriverInterface $driver)
+    {
+        parent::__construct(
+            new Container(['driver' => $driver]),
+            'default',
+            'driver'
+        );
+    }
+}
diff --git a/Symfony/Consumption/ChooseLoggerCommandTrait.php b/Symfony/Consumption/ChooseLoggerCommandTrait.php
new file mode 100644
index 0000000..c229c14
--- /dev/null
+++ b/Symfony/Consumption/ChooseLoggerCommandTrait.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Enqueue\Symfony\Consumption;
+
+use Enqueue\Consumption\Extension\LoggerExtension;
+use Psr\Log\NullLogger;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Logger\ConsoleLogger;
+use Symfony\Component\Console\Output\OutputInterface;
+
+trait ChooseLoggerCommandTrait
+{
+    protected function configureLoggerExtension(): void
+    {
+        $this
+            ->addOption('logger', null, InputOption::VALUE_OPTIONAL, 'A logger to be used. Could be "default", "null", "stdout".', 'default')
+        ;
+    }
+
+    protected function getLoggerExtension(InputInterface $input, OutputInterface $output): ?LoggerExtension
+    {
+        $logger = $input->getOption('logger');
+        switch ($logger) {
+            case 'null':
+                return new LoggerExtension(new NullLogger());
+            case 'stdout':
+                return new LoggerExtension(new ConsoleLogger($output));
+            case 'default':
+                return null;
+            default:
+                throw new \LogicException(sprintf('The logger "%s" is not supported', $logger));
+        }
+    }
+}
diff --git a/Symfony/Consumption/ConfigurableConsumeCommand.php b/Symfony/Consumption/ConfigurableConsumeCommand.php
new file mode 100644
index 0000000..34cb66d
--- /dev/null
+++ b/Symfony/Consumption/ConfigurableConsumeCommand.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Enqueue\Symfony\Consumption;
+
+use Enqueue\Consumption\ChainExtension;
+use Enqueue\Consumption\QueueConsumerInterface;
+use Enqueue\Consumption\QueueSubscriberInterface;
+use Enqueue\ProcessorRegistryInterface;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand('enqueue:transport:consume')]
+class ConfigurableConsumeCommand extends Command
+{
+    use ChooseLoggerCommandTrait;
+    use LimitsExtensionsCommandTrait;
+    use QueueConsumerOptionsCommandTrait;
+
+    /**
+     * @var ContainerInterface
+     */
+    private $container;
+
+    /**
+     * @var string
+     */
+    private $defaultTransport;
+
+    /**
+     * @var string
+     */
+    private $queueConsumerIdPattern;
+
+    /**
+     * @var string
+     */
+    private $processorRegistryIdPattern;
+
+    public function __construct(
+        ContainerInterface $container,
+        string $defaultTransport,
+        string $queueConsumerIdPattern = 'enqueue.transport.%s.queue_consumer',
+        string $processorRegistryIdPattern = 'enqueue.transport.%s.processor_registry',
+    ) {
+        $this->container = $container;
+        $this->defaultTransport = $defaultTransport;
+        $this->queueConsumerIdPattern = $queueConsumerIdPattern;
+        $this->processorRegistryIdPattern = $processorRegistryIdPattern;
+
+        parent::__construct();
+    }
+
+    protected function configure(): void
+    {
+        $this->configureLimitsExtensions();
+        $this->configureQueueConsumerOptions();
+        $this->configureLoggerExtension();
+
+        $this
+            ->setDescription('A worker that consumes message from a broker. '.
+                'To use this broker you have to explicitly set a queue to consume from '.
+                'and a message processor service')
+            ->addArgument('processor', InputArgument::REQUIRED, 'A message processor.')
+            ->addArgument('queues', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'A queue to consume from', [])
+            ->addOption('transport', 't', InputOption::VALUE_OPTIONAL, 'The transport to consume messages from.', $this->defaultTransport)
+        ;
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $transport = $input->getOption('transport');
+
+        try {
+            $consumer = $this->getQueueConsumer($transport);
+        } catch (NotFoundExceptionInterface $e) {
+            throw new \LogicException(sprintf('Transport "%s" is not supported.', $transport), previous: $e);
+        }
+
+        $this->setQueueConsumerOptions($consumer, $input);
+
+        $processor = $this->getProcessorRegistry($transport)->get($input->getArgument('processor'));
+
+        $queues = $input->getArgument('queues');
+        if (empty($queues) && $processor instanceof QueueSubscriberInterface) {
+            $queues = $processor::getSubscribedQueues();
+        }
+
+        if (empty($queues)) {
+            throw new \LogicException(sprintf('The queue is not provided. The processor must implement "%s" interface and it must return not empty array of queues or a queue set using as a second argument.', QueueSubscriberInterface::class));
+        }
+
+        $extensions = $this->getLimitsExtensions($input, $output);
+
+        if ($loggerExtension = $this->getLoggerExtension($input, $output)) {
+            array_unshift($extensions, $loggerExtension);
+        }
+
+        foreach ($queues as $queue) {
+            $consumer->bind($queue, $processor);
+        }
+
+        $consumer->consume(new ChainExtension($extensions));
+
+        return 0;
+    }
+
+    private function getQueueConsumer(string $name): QueueConsumerInterface
+    {
+        return $this->container->get(sprintf($this->queueConsumerIdPattern, $name));
+    }
+
+    private function getProcessorRegistry(string $name): ProcessorRegistryInterface
+    {
+        return $this->container->get(sprintf($this->processorRegistryIdPattern, $name));
+    }
+}
diff --git a/Symfony/Consumption/ConsumeCommand.php b/Symfony/Consumption/ConsumeCommand.php
new file mode 100644
index 0000000..b69ae72
--- /dev/null
+++ b/Symfony/Consumption/ConsumeCommand.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Enqueue\Symfony\Consumption;
+
+use Enqueue\Consumption\ChainExtension;
+use Enqueue\Consumption\Extension\ExitStatusExtension;
+use Enqueue\Consumption\QueueConsumerInterface;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand('enqueue:transport:consume')]
+class ConsumeCommand extends Command
+{
+    use ChooseLoggerCommandTrait;
+    use LimitsExtensionsCommandTrait;
+    use QueueConsumerOptionsCommandTrait;
+
+    /**
+     * @var ContainerInterface
+     */
+    private $container;
+
+    /**
+     * @var string
+     */
+    private $defaultTransport;
+
+    /**
+     * @var string
+     */
+    private $queueConsumerIdPattern;
+
+    public function __construct(ContainerInterface $container, string $defaultTransport, string $queueConsumerIdPattern = 'enqueue.transport.%s.queue_consumer')
+    {
+        $this->container = $container;
+        $this->defaultTransport = $defaultTransport;
+        $this->queueConsumerIdPattern = $queueConsumerIdPattern;
+
+        parent::__construct();
+    }
+
+    protected function configure(): void
+    {
+        $this->configureLimitsExtensions();
+        $this->configureQueueConsumerOptions();
+        $this->configureLoggerExtension();
+
+        $this
+            ->addOption('transport', 't', InputOption::VALUE_OPTIONAL, 'The transport to consume messages from.', $this->defaultTransport)
+            ->setDescription('A worker that consumes message from a broker. '.
+                'To use this broker you have to configure queue consumer before adding to the command')
+        ;
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $transport = $input->getOption('transport');
+
+        try {
+            // QueueConsumer must be pre configured outside of the command!
+            $consumer = $this->getQueueConsumer($transport);
+        } catch (NotFoundExceptionInterface $e) {
+            throw new \LogicException(sprintf('Transport "%s" is not supported.', $transport), previous: $e);
+        }
+
+        $this->setQueueConsumerOptions($consumer, $input);
+
+        $extensions = $this->getLimitsExtensions($input, $output);
+
+        if ($loggerExtension = $this->getLoggerExtension($input, $output)) {
+            array_unshift($extensions, $loggerExtension);
+        }
+
+        $exitStatusExtension = new ExitStatusExtension();
+        array_unshift($extensions, $exitStatusExtension);
+
+        $consumer->consume(new ChainExtension($extensions));
+
+        return $exitStatusExtension->getExitStatus() ?? 0;
+    }
+
+    private function getQueueConsumer(string $name): QueueConsumerInterface
+    {
+        return $this->container->get(sprintf($this->queueConsumerIdPattern, $name));
+    }
+}
diff --git a/Symfony/Consumption/ConsumeMessagesCommand.php b/Symfony/Consumption/ConsumeMessagesCommand.php
deleted file mode 100644
index c2c7695..0000000
--- a/Symfony/Consumption/ConsumeMessagesCommand.php
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php
-
-namespace Enqueue\Symfony\Consumption;
-
-use Enqueue\Consumption\ChainExtension;
-use Enqueue\Consumption\Extension\LoggerExtension;
-use Enqueue\Consumption\QueueConsumer;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Logger\ConsoleLogger;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\DependencyInjection\ContainerAwareInterface;
-use Symfony\Component\DependencyInjection\ContainerAwareTrait;
-
-class ConsumeMessagesCommand extends Command implements ContainerAwareInterface
-{
-    use ContainerAwareTrait;
-    use LimitsExtensionsCommandTrait;
-    use QueueConsumerOptionsCommandTrait;
-
-    /**
-     * @var QueueConsumer
-     */
-    protected $consumer;
-
-    /**
-     * @param QueueConsumer $consumer
-     */
-    public function __construct(QueueConsumer $consumer)
-    {
-        parent::__construct(null);
-
-        $this->consumer = $consumer;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function configure()
-    {
-        $this->configureLimitsExtensions();
-        $this->configureQueueConsumerOptions();
-
-        $this
-            ->setName('enqueue:transport:consume')
-            ->setDescription('A worker that consumes message from a broker. '.
-                'To use this broker you have to configure queue consumer before adding to the command')
-        ;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function execute(InputInterface $input, OutputInterface $output)
-    {
-        $this->setQueueConsumerOptions($this->consumer, $input);
-
-        $extensions = $this->getLimitsExtensions($input, $output);
-        array_unshift($extensions, new LoggerExtension(new ConsoleLogger($output)));
-
-        $runtimeExtensions = new ChainExtension($extensions);
-
-        $this->consumer->consume($runtimeExtensions);
-    }
-}
diff --git a/Symfony/Consumption/ContainerAwareConsumeMessagesCommand.php b/Symfony/Consumption/ContainerAwareConsumeMessagesCommand.php
deleted file mode 100644
index 297dfa5..0000000
--- a/Symfony/Consumption/ContainerAwareConsumeMessagesCommand.php
+++ /dev/null
@@ -1,100 +0,0 @@
-<?php
-
-namespace Enqueue\Symfony\Consumption;
-
-use Enqueue\Consumption\ChainExtension;
-use Enqueue\Consumption\Extension\LoggerExtension;
-use Enqueue\Consumption\QueueConsumer;
-use Enqueue\Consumption\QueueSubscriberInterface;
-use Interop\Queue\PsrProcessor;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputArgument;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Input\InputOption;
-use Symfony\Component\Console\Logger\ConsoleLogger;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\DependencyInjection\ContainerAwareInterface;
-use Symfony\Component\DependencyInjection\ContainerAwareTrait;
-
-class ContainerAwareConsumeMessagesCommand extends Command implements ContainerAwareInterface
-{
-    use ContainerAwareTrait;
-    use LimitsExtensionsCommandTrait;
-    use QueueConsumerOptionsCommandTrait;
-
-    /**
-     * @var QueueConsumer
-     */
-    protected $consumer;
-
-    /**
-     * ConsumeMessagesCommand constructor.
-     *
-     * @param QueueConsumer $consumer
-     */
-    public function __construct(QueueConsumer $consumer)
-    {
-        parent::__construct(null);
-
-        $this->consumer = $consumer;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function configure()
-    {
-        $this->configureLimitsExtensions();
-        $this->configureQueueConsumerOptions();
-
-        $this
-            ->setName('enqueue:transport:consume')
-            ->setDescription('A worker that consumes message from a broker. '.
-                'To use this broker you have to explicitly set a queue to consume from '.
-                'and a message processor service')
-            ->addArgument('processor-service', InputArgument::REQUIRED, 'A message processor service')
-            ->addOption('queue', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Queues to consume from', [])
-        ;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    protected function execute(InputInterface $input, OutputInterface $output)
-    {
-        $this->setQueueConsumerOptions($this->consumer, $input);
-
-        /** @var PsrProcessor $processor */
-        $processor = $this->container->get($input->getArgument('processor-service'));
-        if (false == $processor instanceof  PsrProcessor) {
-            throw new \LogicException(sprintf(
-                'Invalid message processor service given. It must be an instance of %s but %s',
-                PsrProcessor::class,
-                get_class($processor)
-            ));
-        }
-
-        $queues = $input->getOption('queue');
-        if (empty($queues) && $processor instanceof QueueSubscriberInterface) {
-            $queues = $processor::getSubscribedQueues();
-        }
-
-        if (empty($queues)) {
-            throw new \LogicException(sprintf(
-                'The queues are not provided. The processor must implement "%s" interface and it must return not empty array of queues or queues set using --queue option.',
-                QueueSubscriberInterface::class
-            ));
-        }
-
-        $extensions = $this->getLimitsExtensions($input, $output);
-        array_unshift($extensions, new LoggerExtension(new ConsoleLogger($output)));
-
-        $runtimeExtensions = new ChainExtension($extensions);
-
-        foreach ($queues as $queue) {
-            $this->consumer->bind($queue, $processor);
-        }
-
-        $this->consumer->consume($runtimeExtensions);
-    }
-}
diff --git a/Symfony/Consumption/LimitsExtensionsCommandTrait.php b/Symfony/Consumption/LimitsExtensionsCommandTrait.php
index 34bc76d..d8351ac 100644
--- a/Symfony/Consumption/LimitsExtensionsCommandTrait.php
+++ b/Symfony/Consumption/LimitsExtensionsCommandTrait.php
@@ -5,6 +5,7 @@
 use Enqueue\Consumption\Extension\LimitConsumedMessagesExtension;
 use Enqueue\Consumption\Extension\LimitConsumerMemoryExtension;
 use Enqueue\Consumption\Extension\LimitConsumptionTimeExtension;
+use Enqueue\Consumption\Extension\NicenessExtension;
 use Enqueue\Consumption\ExtensionInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -12,21 +13,16 @@
 
 trait LimitsExtensionsCommandTrait
 {
-    /**
-     * {@inheritdoc}
-     */
     protected function configureLimitsExtensions()
     {
         $this
             ->addOption('message-limit', null, InputOption::VALUE_REQUIRED, 'Consume n messages and exit')
             ->addOption('time-limit', null, InputOption::VALUE_REQUIRED, 'Consume messages during this time')
-            ->addOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Consume messages until process reaches this memory limit in MB');
+            ->addOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Consume messages until process reaches this memory limit in MB')
+            ->addOption('niceness', null, InputOption::VALUE_REQUIRED, 'Set process niceness');
     }
 
     /**
-     * @param InputInterface  $input
-     * @param OutputInterface $output
-     *
      * @throws \Exception
      *
      * @return ExtensionInterface[]
@@ -58,6 +54,11 @@ protected function getLimitsExtensions(InputInterface $input, OutputInterface $o
             $extensions[] = new LimitConsumerMemoryExtension($memoryLimit);
         }
 
+        $niceness = $input->getOption('niceness');
+        if (!empty($niceness) && is_numeric($niceness)) {
+            $extensions[] = new NicenessExtension((int) $niceness);
+        }
+
         return $extensions;
     }
 }
diff --git a/Symfony/Consumption/QueueConsumerOptionsCommandTrait.php b/Symfony/Consumption/QueueConsumerOptionsCommandTrait.php
index c6ffd98..fd736f2 100644
--- a/Symfony/Consumption/QueueConsumerOptionsCommandTrait.php
+++ b/Symfony/Consumption/QueueConsumerOptionsCommandTrait.php
@@ -2,33 +2,21 @@
 
 namespace Enqueue\Symfony\Consumption;
 
-use Enqueue\Consumption\QueueConsumer;
+use Enqueue\Consumption\QueueConsumerInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
 
 trait QueueConsumerOptionsCommandTrait
 {
-    /**
-     * {@inheritdoc}
-     */
     protected function configureQueueConsumerOptions()
     {
         $this
-            ->addOption('idle-timeout', null, InputOption::VALUE_REQUIRED, 'The time in milliseconds queue consumer idle if no message has been received.')
             ->addOption('receive-timeout', null, InputOption::VALUE_REQUIRED, 'The time in milliseconds queue consumer waits for a message.')
         ;
     }
 
-    /**
-     * @param QueueConsumer  $consumer
-     * @param InputInterface $input
-     */
-    protected function setQueueConsumerOptions(QueueConsumer $consumer, InputInterface $input)
+    protected function setQueueConsumerOptions(QueueConsumerInterface $consumer, InputInterface $input)
     {
-        if (null !== $idleTimeout = $input->getOption('idle-timeout')) {
-            $consumer->setIdleTimeout((int) $idleTimeout);
-        }
-
         if (null !== $receiveTimeout = $input->getOption('receive-timeout')) {
             $consumer->setReceiveTimeout((int) $receiveTimeout);
         }
diff --git a/Symfony/Consumption/SimpleConsumeCommand.php b/Symfony/Consumption/SimpleConsumeCommand.php
new file mode 100644
index 0000000..90d0e36
--- /dev/null
+++ b/Symfony/Consumption/SimpleConsumeCommand.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Enqueue\Symfony\Consumption;
+
+use Enqueue\Consumption\QueueConsumerInterface;
+use Enqueue\Container\Container;
+
+class SimpleConsumeCommand extends ConsumeCommand
+{
+    public function __construct(QueueConsumerInterface $consumer)
+    {
+        parent::__construct(
+            new Container(['queue_consumer' => $consumer]),
+            'default',
+            'queue_consumer'
+        );
+    }
+}
diff --git a/Symfony/ContainerProcessorRegistry.php b/Symfony/ContainerProcessorRegistry.php
new file mode 100644
index 0000000..b259d23
--- /dev/null
+++ b/Symfony/ContainerProcessorRegistry.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Enqueue\Symfony;
+
+use Enqueue\ProcessorRegistryInterface;
+use Interop\Queue\Processor;
+use Psr\Container\ContainerInterface;
+
+final class ContainerProcessorRegistry implements ProcessorRegistryInterface
+{
+    /**
+     * @var ContainerInterface
+     */
+    private $locator;
+
+    public function __construct(ContainerInterface $locator)
+    {
+        $this->locator = $locator;
+    }
+
+    public function get(string $processorName): Processor
+    {
+        if (false == $this->locator->has($processorName)) {
+            throw new \LogicException(sprintf('Service locator does not have a processor with name "%s".', $processorName));
+        }
+
+        return $this->locator->get($processorName);
+    }
+}
diff --git a/Symfony/DefaultTransportFactory.php b/Symfony/DefaultTransportFactory.php
deleted file mode 100644
index 2e33444..0000000
--- a/Symfony/DefaultTransportFactory.php
+++ /dev/null
@@ -1,230 +0,0 @@
-<?php
-
-namespace Enqueue\Symfony;
-
-use Enqueue\AmqpBunny\AmqpConnectionFactory as AmqpBunnyConnectionFactory;
-use Enqueue\AmqpBunny\Symfony\AmqpBunnyTransportFactory;
-use Enqueue\AmqpExt\AmqpConnectionFactory as AmqpExtConnectionFactory;
-use Enqueue\AmqpExt\Symfony\AmqpTransportFactory;
-use Enqueue\AmqpLib\AmqpConnectionFactory as AmqpLibConnectionFactory;
-use Enqueue\AmqpLib\Symfony\AmqpLibTransportFactory;
-use Enqueue\Dbal\DbalConnectionFactory;
-use Enqueue\Dbal\Symfony\DbalTransportFactory;
-use Enqueue\Fs\FsConnectionFactory;
-use Enqueue\Fs\Symfony\FsTransportFactory;
-use Enqueue\Gps\GpsConnectionFactory;
-use Enqueue\Gps\Symfony\GpsTransportFactory;
-use Enqueue\Null\NullConnectionFactory;
-use Enqueue\Null\Symfony\NullTransportFactory;
-use Enqueue\Redis\RedisConnectionFactory;
-use Enqueue\Redis\Symfony\RedisTransportFactory;
-use Enqueue\Sqs\SqsConnectionFactory;
-use Enqueue\Sqs\Symfony\SqsTransportFactory;
-use Enqueue\Stomp\StompConnectionFactory;
-use Enqueue\Stomp\Symfony\StompTransportFactory;
-use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-use function Enqueue\dsn_to_connection_factory;
-
-class DefaultTransportFactory implements TransportFactoryInterface, DriverFactoryInterface
-{
-    /**
-     * @var string
-     */
-    private $name;
-
-    /**
-     * @param string $name
-     */
-    public function __construct($name = 'default')
-    {
-        $this->name = $name;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function addConfiguration(ArrayNodeDefinition $builder)
-    {
-        $builder
-            ->beforeNormalization()
-                ->always(function ($v) {
-                    if (is_array($v)) {
-                        if (empty($v['dsn']) && empty($v['alias'])) {
-                            throw new \LogicException('Either dsn or alias option must be set');
-                        }
-
-                        return $v;
-                    }
-
-                    if (empty($v)) {
-                        return ['dsn' => 'null:'];
-                    }
-
-                    if (is_string($v)) {
-                        return false !== strpos($v, '://') || false !== strpos($v, 'env_') ?
-                            ['dsn' => $v] :
-                            ['alias' => $v];
-                    }
-                })
-            ->end()
-            ->children()
-                ->scalarNode('alias')->cannotBeEmpty()->end()
-                ->scalarNode('dsn')->cannotBeEmpty()->end()
-            ->end()
-        ->end()
-        ;
-    }
-
-    public function createConnectionFactory(ContainerBuilder $container, array $config)
-    {
-        if (isset($config['alias'])) {
-            $aliasId = sprintf('enqueue.transport.%s.connection_factory', $config['alias']);
-        } else {
-            $dsn = $this->resolveDSN($container, $config['dsn']);
-
-            $aliasId = $this->findFactory($dsn)->createConnectionFactory($container, $config);
-        }
-
-        $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName());
-
-        $container->setAlias($factoryId, $aliasId);
-        $container->setAlias('enqueue.transport.connection_factory', $factoryId);
-
-        return $factoryId;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function createContext(ContainerBuilder $container, array $config)
-    {
-        if (isset($config['alias'])) {
-            $aliasId = sprintf('enqueue.transport.%s.context', $config['alias']);
-        } else {
-            $dsn = $this->resolveDSN($container, $config['dsn']);
-
-            $aliasId = $this->findFactory($dsn)->createContext($container, $config);
-        }
-
-        $contextId = sprintf('enqueue.transport.%s.context', $this->getName());
-
-        $container->setAlias($contextId, $aliasId);
-        $container->setAlias('enqueue.transport.context', $contextId);
-
-        return $contextId;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function createDriver(ContainerBuilder $container, array $config)
-    {
-        if (isset($config['alias'])) {
-            $aliasId = sprintf('enqueue.client.%s.driver', $config['alias']);
-        } else {
-            $dsn = $this->resolveDSN($container, $config['dsn']);
-
-            $aliasId = $this->findFactory($dsn)->createDriver($container, $config);
-        }
-
-        $driverId = sprintf('enqueue.client.%s.driver', $this->getName());
-
-        $container->setAlias($driverId, $aliasId);
-        $container->setAlias('enqueue.client.driver', $driverId);
-
-        return $driverId;
-    }
-
-    /**
-     * @return string
-     */
-    public function getName()
-    {
-        return $this->name;
-    }
-
-    /**
-     * This is a quick fix to the exception "Incompatible use of dynamic environment variables "ENQUEUE_DSN" found in parameters."
-     * TODO: We'll have to come up with a better solution.
-     *
-     * @param ContainerBuilder $container
-     * @param $dsn
-     *
-     * @return array|false|string
-     */
-    private function resolveDSN(ContainerBuilder $container, $dsn)
-    {
-        if (method_exists($container, 'resolveEnvPlaceholders')) {
-            $dsn = $container->resolveEnvPlaceholders($dsn);
-
-            $matches = [];
-            if (preg_match('/%env\((.*?)\)/', $dsn, $matches)) {
-                if (false === $realDsn = getenv($matches[1])) {
-                    throw new \LogicException(sprintf('The env "%s" var is not defined', $matches[1]));
-                }
-
-                return $realDsn;
-            }
-        }
-
-        return $dsn;
-    }
-
-    /**
-     * @param string
-     * @param mixed $dsn
-     *
-     * @return TransportFactoryInterface
-     */
-    private function findFactory($dsn)
-    {
-        $factory = dsn_to_connection_factory($dsn);
-
-        if ($factory instanceof AmqpExtConnectionFactory) {
-            return new AmqpTransportFactory('default_amqp_ext');
-        }
-
-        if ($factory instanceof AmqpLibConnectionFactory) {
-            return new AmqpLibTransportFactory('default_amqp_lib');
-        }
-
-        if ($factory instanceof AmqpBunnyConnectionFactory) {
-            return new AmqpBunnyTransportFactory('default_amqp_bunny');
-        }
-
-        if ($factory instanceof FsConnectionFactory) {
-            return new FsTransportFactory('default_fs');
-        }
-
-        if ($factory instanceof DbalConnectionFactory) {
-            return new DbalTransportFactory('default_dbal');
-        }
-
-        if ($factory instanceof NullConnectionFactory) {
-            return new NullTransportFactory('default_null');
-        }
-
-        if ($factory instanceof GpsConnectionFactory) {
-            return new GpsTransportFactory('default_gps');
-        }
-
-        if ($factory instanceof RedisConnectionFactory) {
-            return new RedisTransportFactory('default_redis');
-        }
-
-        if ($factory instanceof SqsConnectionFactory) {
-            return new SqsTransportFactory('default_sqs');
-        }
-
-        if ($factory instanceof StompConnectionFactory) {
-            return new StompTransportFactory('default_stomp');
-        }
-
-        throw new \LogicException(sprintf(
-            'There is no supported transport factory for the connection factory "%s" created from DSN "%s"',
-            get_class($factory),
-            $dsn
-        ));
-    }
-}
diff --git a/Symfony/DependencyInjection/BuildConsumptionExtensionsPass.php b/Symfony/DependencyInjection/BuildConsumptionExtensionsPass.php
new file mode 100644
index 0000000..99f274e
--- /dev/null
+++ b/Symfony/DependencyInjection/BuildConsumptionExtensionsPass.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Enqueue\Symfony\DependencyInjection;
+
+use Enqueue\Symfony\DiUtils;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+
+final class BuildConsumptionExtensionsPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container): void
+    {
+        if (false == $container->hasParameter('enqueue.transports')) {
+            throw new \LogicException('The "enqueue.transports" parameter must be set.');
+        }
+
+        $names = $container->getParameter('enqueue.transports');
+        $defaultName = $container->getParameter('enqueue.default_transport');
+
+        foreach ($names as $name) {
+            $diUtils = DiUtils::create(TransportFactory::MODULE, $name);
+
+            $extensionsId = $diUtils->format('consumption_extensions');
+            if (false == $container->hasDefinition($extensionsId)) {
+                throw new \LogicException(sprintf('Service "%s" not found', $extensionsId));
+            }
+
+            $tags = $container->findTaggedServiceIds('enqueue.transport.consumption_extension');
+
+            $groupByPriority = [];
+            foreach ($tags as $serviceId => $tagAttributes) {
+                foreach ($tagAttributes as $tagAttribute) {
+                    $transport = $tagAttribute['transport'] ?? $defaultName;
+
+                    if ($transport !== $name && 'all' !== $transport) {
+                        continue;
+                    }
+
+                    $priority = (int) ($tagAttribute['priority'] ?? 0);
+
+                    $groupByPriority[$priority][] = new Reference($serviceId);
+                }
+            }
+
+            krsort($groupByPriority, \SORT_NUMERIC);
+
+            $flatExtensions = [];
+            foreach ($groupByPriority as $extension) {
+                $flatExtensions = array_merge($flatExtensions, $extension);
+            }
+
+            $extensionsService = $container->getDefinition($extensionsId);
+            $extensionsService->replaceArgument(0, array_merge(
+                $extensionsService->getArgument(0),
+                $flatExtensions
+            ));
+        }
+    }
+}
diff --git a/Symfony/DependencyInjection/BuildProcessorRegistryPass.php b/Symfony/DependencyInjection/BuildProcessorRegistryPass.php
new file mode 100644
index 0000000..cc6e042
--- /dev/null
+++ b/Symfony/DependencyInjection/BuildProcessorRegistryPass.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Enqueue\Symfony\DependencyInjection;
+
+use Enqueue\Symfony\DiUtils;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+
+final class BuildProcessorRegistryPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container): void
+    {
+        if (false == $container->hasParameter('enqueue.transports')) {
+            throw new \LogicException('The "enqueue.transports" parameter must be set.');
+        }
+
+        $names = $container->getParameter('enqueue.transports');
+        $defaultName = $container->getParameter('enqueue.default_transport');
+
+        foreach ($names as $name) {
+            $diUtils = DiUtils::create(TransportFactory::MODULE, $name);
+
+            $processorRegistryId = $diUtils->format('processor_registry');
+            if (false == $container->hasDefinition($processorRegistryId)) {
+                throw new \LogicException(sprintf('Service "%s" not found', $processorRegistryId));
+            }
+
+            $tag = 'enqueue.transport.processor';
+            $map = [];
+            foreach ($container->findTaggedServiceIds($tag) as $serviceId => $tagAttributes) {
+                foreach ($tagAttributes as $tagAttribute) {
+                    $transport = $tagAttribute['transport'] ?? $defaultName;
+
+                    if ($transport !== $name && 'all' !== $transport) {
+                        continue;
+                    }
+
+                    $processor = $tagAttribute['processor'] ?? $serviceId;
+
+                    $map[$processor] = new Reference($serviceId);
+                }
+            }
+
+            $registry = $container->getDefinition($processorRegistryId);
+            $registry->setArgument(0, ServiceLocatorTagPass::register($container, $map, $processorRegistryId));
+        }
+    }
+}
diff --git a/Symfony/DependencyInjection/TransportFactory.php b/Symfony/DependencyInjection/TransportFactory.php
new file mode 100644
index 0000000..944b1a3
--- /dev/null
+++ b/Symfony/DependencyInjection/TransportFactory.php
@@ -0,0 +1,266 @@
+<?php
+
+namespace Enqueue\Symfony\DependencyInjection;
+
+use Enqueue\ConnectionFactoryFactory;
+use Enqueue\ConnectionFactoryFactoryInterface;
+use Enqueue\Consumption\ChainExtension;
+use Enqueue\Consumption\Extension\LogExtension;
+use Enqueue\Consumption\QueueConsumer;
+use Enqueue\Consumption\QueueConsumerInterface;
+use Enqueue\Resources;
+use Enqueue\Rpc\RpcClient;
+use Enqueue\Rpc\RpcFactory;
+use Enqueue\Symfony\ContainerProcessorRegistry;
+use Enqueue\Symfony\DiUtils;
+use Interop\Queue\ConnectionFactory;
+use Interop\Queue\Context;
+use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
+use Symfony\Component\Config\Definition\Builder\NodeDefinition;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * @internal
+ */
+final class TransportFactory
+{
+    public const MODULE = 'transport';
+
+    /**
+     * @var bool
+     */
+    private $default;
+
+    /**
+     * @var DiUtils
+     */
+    private $diUtils;
+
+    public function __construct(string $name, bool $default = false)
+    {
+        if (empty($name)) {
+            throw new \InvalidArgumentException('The name could not be empty.');
+        }
+
+        $this->default = $default;
+        $this->diUtils = DiUtils::create(self::MODULE, $name);
+    }
+
+    public static function getConfiguration(string $name = 'transport'): NodeDefinition
+    {
+        $knownSchemes = array_keys(Resources::getKnownSchemes());
+        $availableSchemes = array_keys(Resources::getAvailableSchemes());
+
+        $builder = new ArrayNodeDefinition($name);
+        $builder
+            ->info('The transport option could accept a string DSN, an array with DSN key, or null. It accept extra options. To find out what option you can set, look at connection factory constructor docblock.')
+            ->beforeNormalization()
+                ->always(function ($v) {
+                    if (empty($v)) {
+                        return ['dsn' => 'null:'];
+                    }
+
+                    if (is_array($v)) {
+                        if (isset($v['factory_class']) && isset($v['factory_service'])) {
+                            throw new \LogicException('Both options factory_class and factory_service are set. Please choose one.');
+                        }
+
+                        if (isset($v['connection_factory_class']) && (isset($v['factory_class']) || isset($v['factory_service']))) {
+                            throw new \LogicException('The option connection_factory_class must not be used with factory_class or factory_service at the same time. Please choose one.');
+                        }
+
+                        return $v;
+                    }
+
+                    if (is_string($v)) {
+                        return ['dsn' => $v];
+                    }
+
+                    throw new \LogicException(sprintf('The value must be array, null or string. Got "%s"', gettype($v)));
+                })
+        ->end()
+        ->isRequired()
+        ->ignoreExtraKeys(false)
+        ->children()
+            ->scalarNode('dsn')
+                ->cannotBeEmpty()
+                ->isRequired()
+                ->info(sprintf(
+                    'The MQ broker DSN. These schemes are supported: "%s", to use these "%s" you have to install a package.',
+                    implode('", "', $knownSchemes),
+                    implode('", "', $availableSchemes)
+                ))
+            ->end()
+            ->scalarNode('connection_factory_class')
+                ->info(sprintf('The connection factory class should implement "%s" interface', ConnectionFactory::class))
+            ->end()
+            ->scalarNode('factory_service')
+                ->info(sprintf('The factory class should implement "%s" interface', ConnectionFactoryFactoryInterface::class))
+            ->end()
+            ->scalarNode('factory_class')
+                ->info(sprintf('The factory service should be a class that implements "%s" interface', ConnectionFactoryFactoryInterface::class))
+            ->end()
+        ->end()
+        ;
+
+        return $builder;
+    }
+
+    public static function getQueueConsumerConfiguration(string $name = 'consumption'): ArrayNodeDefinition
+    {
+        $builder = new ArrayNodeDefinition($name);
+
+        $builder
+            ->addDefaultsIfNotSet()->children()
+                ->integerNode('receive_timeout')
+                    ->min(0)
+                    ->defaultValue(10000)
+                    ->info('the time in milliseconds queue consumer waits for a message (10000 ms by default)')
+                ->end()
+        ;
+
+        return $builder;
+    }
+
+    public function buildConnectionFactory(ContainerBuilder $container, array $config): void
+    {
+        $factoryId = $this->diUtils->format('connection_factory');
+
+        $factoryFactoryId = $this->diUtils->format('connection_factory_factory');
+        $container->register($factoryFactoryId, $config['factory_class'] ?? ConnectionFactoryFactory::class);
+
+        $factoryFactoryService = new Reference(
+            $config['factory_service'] ?? $factoryFactoryId
+        );
+
+        unset($config['factory_service'], $config['factory_class']);
+
+        $connectionFactoryClass = $config['connection_factory_class'] ?? null;
+        unset($config['connection_factory_class']);
+
+        if (isset($connectionFactoryClass)) {
+            $container->register($factoryId, $connectionFactoryClass)
+                ->addArgument($config)
+            ;
+        } else {
+            $container->register($factoryId, ConnectionFactory::class)
+                ->setFactory([$factoryFactoryService, 'create'])
+                ->addArgument($config)
+            ;
+        }
+
+        if ($this->default) {
+            $container->setAlias(ConnectionFactory::class, $factoryId);
+
+            if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) {
+                $container->setAlias($this->diUtils->formatDefault('connection_factory'), $factoryId);
+            }
+        }
+    }
+
+    public function buildContext(ContainerBuilder $container, array $config): void
+    {
+        $factoryId = $this->diUtils->format('connection_factory');
+        $this->assertServiceExists($container, $factoryId);
+
+        $contextId = $this->diUtils->format('context');
+
+        $container->register($contextId, Context::class)
+            ->setFactory([new Reference($factoryId), 'createContext'])
+        ;
+
+        $this->addServiceToLocator($container, 'context');
+
+        if ($this->default) {
+            $container->setAlias(Context::class, $contextId);
+
+            if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) {
+                $container->setAlias($this->diUtils->formatDefault('context'), $contextId);
+            }
+        }
+    }
+
+    public function buildQueueConsumer(ContainerBuilder $container, array $config): void
+    {
+        $contextId = $this->diUtils->format('context');
+        $this->assertServiceExists($container, $contextId);
+
+        $container->setParameter($this->diUtils->format('receive_timeout'), $config['receive_timeout'] ?? 10000);
+
+        $logExtensionId = $this->diUtils->format('log_extension');
+        $container->register($logExtensionId, LogExtension::class)
+            ->addTag('enqueue.transport.consumption_extension', ['transport' => $this->diUtils->getConfigName(), 'priority' => -100])
+        ;
+
+        $container->register($this->diUtils->format('consumption_extensions'), ChainExtension::class)
+            ->addArgument([])
+        ;
+
+        $container->register($this->diUtils->format('queue_consumer'), QueueConsumer::class)
+            ->addArgument(new Reference($contextId))
+            ->addArgument(new Reference($this->diUtils->format('consumption_extensions')))
+            ->addArgument([])
+            ->addArgument(new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE))
+            ->addArgument($this->diUtils->parameter('receive_timeout'))
+        ;
+
+        $container->register($this->diUtils->format('processor_registry'), ContainerProcessorRegistry::class);
+
+        $this->addServiceToLocator($container, 'queue_consumer');
+        $this->addServiceToLocator($container, 'processor_registry');
+
+        if ($this->default) {
+            $container->setAlias(QueueConsumerInterface::class, $this->diUtils->format('queue_consumer'));
+
+            if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) {
+                $container->setAlias($this->diUtils->formatDefault('queue_consumer'), $this->diUtils->format('queue_consumer'));
+            }
+        }
+    }
+
+    public function buildRpcClient(ContainerBuilder $container, array $config): void
+    {
+        $contextId = $this->diUtils->format('context');
+        $this->assertServiceExists($container, $contextId);
+
+        $container->register($this->diUtils->format('rpc_factory'), RpcFactory::class)
+            ->addArgument(new Reference($contextId))
+        ;
+
+        $container->register($this->diUtils->format('rpc_client'), RpcClient::class)
+            ->addArgument(new Reference($contextId))
+            ->addArgument(new Reference($this->diUtils->format('rpc_factory')))
+        ;
+
+        if ($this->default) {
+            $container->setAlias(RpcClient::class, $this->diUtils->format('rpc_client'));
+
+            if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) {
+                $container->setAlias($this->diUtils->formatDefault('rpc_client'), $this->diUtils->format('rpc_client'));
+            }
+        }
+    }
+
+    private function assertServiceExists(ContainerBuilder $container, string $serviceId): void
+    {
+        if (false == $container->hasDefinition($serviceId)) {
+            throw new \InvalidArgumentException(sprintf('The service "%s" does not exist.', $serviceId));
+        }
+    }
+
+    private function addServiceToLocator(ContainerBuilder $container, string $serviceName): void
+    {
+        $locatorId = 'enqueue.locator';
+
+        if ($container->hasDefinition($locatorId)) {
+            $locator = $container->getDefinition($locatorId);
+
+            $map = $locator->getArgument(0);
+            $map[$this->diUtils->format($serviceName)] = $this->diUtils->reference($serviceName);
+
+            $locator->replaceArgument(0, $map);
+        }
+    }
+}
diff --git a/Symfony/DiUtils.php b/Symfony/DiUtils.php
new file mode 100644
index 0000000..be45287
--- /dev/null
+++ b/Symfony/DiUtils.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Enqueue\Symfony;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\Reference;
+
+class DiUtils
+{
+    public const DEFAULT_CONFIG = 'default';
+
+    /**
+     * @var string
+     */
+    private $moduleName;
+
+    /**
+     * @var string
+     */
+    private $configName;
+
+    public function __construct(string $moduleName, string $configName)
+    {
+        $this->moduleName = $moduleName;
+        $this->configName = $configName;
+    }
+
+    public static function create(string $moduleName, string $configName): self
+    {
+        return new self($moduleName, $configName);
+    }
+
+    public function getModuleName(): string
+    {
+        return $this->moduleName;
+    }
+
+    public function getConfigName(): string
+    {
+        return $this->configName;
+    }
+
+    public function reference(string $serviceName, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): Reference
+    {
+        return new Reference($this->format($serviceName), $invalidBehavior);
+    }
+
+    public function referenceDefault(string $serviceName, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): Reference
+    {
+        return new Reference($this->formatDefault($serviceName), $invalidBehavior);
+    }
+
+    public function parameter(string $serviceName): string
+    {
+        $fullName = $this->format($serviceName);
+
+        return "%$fullName%";
+    }
+
+    public function parameterDefault(string $serviceName): string
+    {
+        $fullName = $this->formatDefault($serviceName);
+
+        return "%$fullName%";
+    }
+
+    public function format(string $serviceName): string
+    {
+        return $this->doFormat($this->moduleName, $this->configName, $serviceName);
+    }
+
+    public function formatDefault(string $serviceName): string
+    {
+        return $this->doFormat($this->moduleName, self::DEFAULT_CONFIG, $serviceName);
+    }
+
+    private function doFormat(string $moduleName, string $configName, string $serviceName): string
+    {
+        return sprintf('enqueue.%s.%s.%s', $moduleName, $configName, $serviceName);
+    }
+}
diff --git a/Symfony/DriverFactoryInterface.php b/Symfony/DriverFactoryInterface.php
deleted file mode 100644
index 0b1ab02..0000000
--- a/Symfony/DriverFactoryInterface.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-namespace Enqueue\Symfony;
-
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-
-interface DriverFactoryInterface
-{
-    /**
-     * @param ContainerBuilder $container
-     * @param array            $config
-     *
-     * @return string The method must return a driver service id
-     */
-    public function createDriver(ContainerBuilder $container, array $config);
-
-    /**
-     * @return string
-     */
-    public function getName();
-}
diff --git a/Symfony/MissingComponentFactory.php b/Symfony/MissingComponentFactory.php
new file mode 100644
index 0000000..94fcb83
--- /dev/null
+++ b/Symfony/MissingComponentFactory.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Enqueue\Symfony;
+
+use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
+
+class MissingComponentFactory
+{
+    public static function getConfiguration(string $componentName, array $packages): ArrayNodeDefinition
+    {
+        if (1 == count($packages)) {
+            $message = sprintf(
+                'In order to use the component "%s" install a package "%s"',
+                $componentName,
+                implode('", "', $packages)
+            );
+        } else {
+            $message = sprintf(
+                'In order to use the component "%s" install one of the packages "%s"',
+                $componentName,
+                implode('", "', $packages)
+            );
+        }
+
+        $node = new ArrayNodeDefinition($componentName);
+        $node
+            ->info($message)
+            ->beforeNormalization()
+                ->always(function () {
+                    return [];
+                })
+            ->end()
+            ->validate()
+                ->always(function () use ($message) {
+                    throw new \InvalidArgumentException($message);
+                })
+            ->end()
+        ;
+
+        return $node;
+    }
+}
diff --git a/Symfony/TransportFactoryInterface.php b/Symfony/TransportFactoryInterface.php
deleted file mode 100644
index d2be801..0000000
--- a/Symfony/TransportFactoryInterface.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-namespace Enqueue\Symfony;
-
-use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-
-interface TransportFactoryInterface
-{
-    /**
-     * @param ArrayNodeDefinition $builder
-     */
-    public function addConfiguration(ArrayNodeDefinition $builder);
-
-    /**
-     * @param ContainerBuilder $container
-     * @param array            $config
-     *
-     * @return string The method must return a factory service id
-     */
-    public function createConnectionFactory(ContainerBuilder $container, array $config);
-
-    /**
-     * @param ContainerBuilder $container
-     * @param array            $config
-     *
-     * @return string The method must return a context service id
-     */
-    public function createContext(ContainerBuilder $container, array $config);
-
-    /**
-     * @return string
-     */
-    public function getName();
-}
diff --git a/Tests/Client/ArrayProcessorRegistryTest.php b/Tests/ArrayProcessorRegistryTest.php
similarity index 77%
rename from Tests/Client/ArrayProcessorRegistryTest.php
rename to Tests/ArrayProcessorRegistryTest.php
index eb5d684..93ad3ec 100644
--- a/Tests/Client/ArrayProcessorRegistryTest.php
+++ b/Tests/ArrayProcessorRegistryTest.php
@@ -1,11 +1,12 @@
 <?php
 
-namespace Enqueue\Tests\Client;
+namespace Enqueue\Tests;
 
-use Enqueue\Client\ArrayProcessorRegistry;
-use Enqueue\Client\ProcessorRegistryInterface;
+use Enqueue\ArrayProcessorRegistry;
+use Enqueue\ProcessorRegistryInterface;
 use Enqueue\Test\ClassExtensionTrait;
-use Interop\Queue\PsrProcessor;
+use Interop\Queue\Processor;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 
 class ArrayProcessorRegistryTest extends TestCase
@@ -17,11 +18,6 @@ public function testShouldImplementProcessorRegistryInterface()
         $this->assertClassImplements(ProcessorRegistryInterface::class, ArrayProcessorRegistry::class);
     }
 
-    public function testCouldBeConstructedWithoutAnyArgument()
-    {
-        new ArrayProcessorRegistry();
-    }
-
     public function testShouldThrowExceptionIfProcessorIsNotSet()
     {
         $registry = new ArrayProcessorRegistry();
@@ -51,10 +47,10 @@ public function testShouldAllowGetProcessorAddedViaAddMethod()
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor
+     * @return MockObject|Processor
      */
     protected function createProcessorMock()
     {
-        return $this->createMock(PsrProcessor::class);
+        return $this->createMock(Processor::class);
     }
 }
diff --git a/Tests/Client/Amqp/AmqpDriverTest.php b/Tests/Client/Amqp/AmqpDriverTest.php
deleted file mode 100644
index a82fda0..0000000
--- a/Tests/Client/Amqp/AmqpDriverTest.php
+++ /dev/null
@@ -1,417 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Client\Amqp;
-
-use Enqueue\Client\Amqp\AmqpDriver;
-use Enqueue\Client\Config;
-use Enqueue\Client\DriverInterface;
-use Enqueue\Client\Message;
-use Enqueue\Client\Meta\QueueMetaRegistry;
-use Enqueue\Test\ClassExtensionTrait;
-use Interop\Amqp\AmqpContext;
-use Interop\Amqp\AmqpProducer;
-use Interop\Amqp\Impl\AmqpBind;
-use Interop\Amqp\Impl\AmqpMessage;
-use Interop\Amqp\Impl\AmqpQueue;
-use Interop\Amqp\Impl\AmqpTopic;
-use PHPUnit\Framework\TestCase;
-
-class AmqpDriverTest extends TestCase
-{
-    use ClassExtensionTrait;
-
-    public function testShouldImplementsDriverInterface()
-    {
-        $this->assertClassImplements(DriverInterface::class, AmqpDriver::class);
-    }
-
-    public function testCouldBeConstructedWithRequiredArguments()
-    {
-        new AmqpDriver(
-            $this->createAmqpContextMock(),
-            $this->createDummyConfig(),
-            $this->createDummyQueueMetaRegistry()
-        );
-    }
-
-    public function testShouldReturnConfigObject()
-    {
-        $config = $this->createDummyConfig();
-
-        $driver = new AmqpDriver($this->createAmqpContextMock(), $config, $this->createDummyQueueMetaRegistry());
-
-        $this->assertSame($config, $driver->getConfig());
-    }
-
-    public function testShouldCreateAndReturnQueueInstance()
-    {
-        $expectedQueue = new AmqpQueue('aName');
-
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createQueue')
-            ->with('aprefix.afooqueue')
-            ->willReturn($expectedQueue)
-        ;
-
-        $driver = new AmqpDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry());
-
-        $queue = $driver->createQueue('aFooQueue');
-
-        $this->assertSame($expectedQueue, $queue);
-        $this->assertSame([], $queue->getArguments());
-        $this->assertSame(2, $queue->getFlags());
-        $this->assertNull($queue->getConsumerTag());
-    }
-
-    public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName()
-    {
-        $expectedQueue = new AmqpQueue('aName');
-
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createQueue')
-            ->with('aBarQueue')
-            ->willReturn($expectedQueue)
-        ;
-
-        $driver = new AmqpDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry());
-
-        $queue = $driver->createQueue('aBarQueue');
-
-        $this->assertSame($expectedQueue, $queue);
-    }
-
-    public function testShouldConvertTransportMessageToClientMessage()
-    {
-        $transportMessage = new AmqpMessage();
-        $transportMessage->setBody('body');
-        $transportMessage->setHeaders(['hkey' => 'hval']);
-        $transportMessage->setProperties(['key' => 'val']);
-        $transportMessage->setHeader('content_type', 'ContentType');
-        $transportMessage->setHeader('expiration', '12345000');
-        $transportMessage->setMessageId('MessageId');
-        $transportMessage->setTimestamp(1000);
-        $transportMessage->setReplyTo('theReplyTo');
-        $transportMessage->setCorrelationId('theCorrelationId');
-
-        $driver = new AmqpDriver(
-            $this->createAmqpContextMock(),
-            $this->createDummyConfig(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $clientMessage = $driver->createClientMessage($transportMessage);
-
-        $this->assertInstanceOf(Message::class, $clientMessage);
-        $this->assertSame('body', $clientMessage->getBody());
-        $this->assertSame([
-            'hkey' => 'hval',
-            'content_type' => 'ContentType',
-            'expiration' => '12345000',
-            'message_id' => 'MessageId',
-            'timestamp' => 1000,
-            'reply_to' => 'theReplyTo',
-            'correlation_id' => 'theCorrelationId',
-        ], $clientMessage->getHeaders());
-        $this->assertSame([
-            'key' => 'val',
-        ], $clientMessage->getProperties());
-        $this->assertSame('MessageId', $clientMessage->getMessageId());
-        $this->assertSame(12345, $clientMessage->getExpire());
-        $this->assertSame('ContentType', $clientMessage->getContentType());
-        $this->assertSame(1000, $clientMessage->getTimestamp());
-        $this->assertSame('theReplyTo', $clientMessage->getReplyTo());
-        $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId());
-    }
-
-    public function testShouldThrowExceptionIfExpirationIsNotNumeric()
-    {
-        $transportMessage = new AmqpMessage();
-        $transportMessage->setHeader('expiration', 'is-not-numeric');
-
-        $driver = new AmqpDriver(
-            $this->createAmqpContextMock(),
-            $this->createDummyConfig(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('expiration header is not numeric. "is-not-numeric"');
-
-        $driver->createClientMessage($transportMessage);
-    }
-
-    public function testShouldConvertClientMessageToTransportMessage()
-    {
-        $clientMessage = new Message();
-        $clientMessage->setBody('body');
-        $clientMessage->setHeaders(['hkey' => 'hval']);
-        $clientMessage->setProperties(['key' => 'val']);
-        $clientMessage->setContentType('ContentType');
-        $clientMessage->setExpire(123);
-        $clientMessage->setMessageId('MessageId');
-        $clientMessage->setTimestamp(1000);
-        $clientMessage->setReplyTo('theReplyTo');
-        $clientMessage->setCorrelationId('theCorrelationId');
-
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createMessage')
-            ->willReturn(new AmqpMessage())
-        ;
-
-        $driver = new AmqpDriver(
-            $context,
-            $this->createDummyConfig(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $transportMessage = $driver->createTransportMessage($clientMessage);
-
-        $this->assertInstanceOf(AmqpMessage::class, $transportMessage);
-        $this->assertSame('body', $transportMessage->getBody());
-        $this->assertSame([
-            'hkey' => 'hval',
-            'message_id' => 'MessageId',
-            'timestamp' => 1000,
-            'reply_to' => 'theReplyTo',
-            'correlation_id' => 'theCorrelationId',
-            'content_type' => 'ContentType',
-            'delivery_mode' => 2,
-            'expiration' => '123000',
-        ], $transportMessage->getHeaders());
-        $this->assertSame([
-            'key' => 'val',
-        ], $transportMessage->getProperties());
-        $this->assertSame('MessageId', $transportMessage->getMessageId());
-        $this->assertSame(1000, $transportMessage->getTimestamp());
-        $this->assertSame('theReplyTo', $transportMessage->getReplyTo());
-        $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId());
-    }
-
-    public function testShouldSendMessageToRouter()
-    {
-        $topic = new AmqpTopic('');
-        $transportMessage = new AmqpMessage();
-
-        $producer = $this->createAmqpProducerMock();
-        $producer
-            ->expects($this->once())
-            ->method('send')
-            ->with($this->identicalTo($topic), $this->identicalTo($transportMessage))
-        ;
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createTopic')
-            ->willReturn($topic)
-        ;
-        $context
-            ->expects($this->once())
-            ->method('createProducer')
-            ->willReturn($producer)
-        ;
-        $context
-            ->expects($this->once())
-            ->method('createMessage')
-            ->willReturn($transportMessage)
-        ;
-
-        $driver = new AmqpDriver(
-            $context,
-            $this->createDummyConfig(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $message = new Message();
-        $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic');
-
-        $driver->sendToRouter($message);
-    }
-
-    public function testShouldThrowExceptionIfTopicParameterIsNotSet()
-    {
-        $driver = new AmqpDriver(
-            $this->createAmqpContextMock(),
-            $this->createDummyConfig(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('Topic name parameter is required but is not set');
-
-        $driver->sendToRouter(new Message());
-    }
-
-    public function testShouldSendMessageToProcessor()
-    {
-        $queue = new AmqpQueue('');
-        $transportMessage = new AmqpMessage();
-
-        $producer = $this->createAmqpProducerMock();
-        $producer
-            ->expects($this->once())
-            ->method('send')
-            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
-        ;
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createQueue')
-            ->willReturn($queue)
-        ;
-        $context
-            ->expects($this->once())
-            ->method('createProducer')
-            ->willReturn($producer)
-        ;
-        $context
-            ->expects($this->once())
-            ->method('createMessage')
-            ->willReturn($transportMessage)
-        ;
-
-        $driver = new AmqpDriver(
-            $context,
-            $this->createDummyConfig(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $message = new Message();
-        $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor');
-        $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue');
-
-        $driver->sendToProcessor($message);
-    }
-
-    public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet()
-    {
-        $driver = new AmqpDriver(
-            $this->createAmqpContextMock(),
-            $this->createDummyConfig(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('Processor name parameter is required but is not set');
-
-        $driver->sendToProcessor(new Message());
-    }
-
-    public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet()
-    {
-        $driver = new AmqpDriver(
-            $this->createAmqpContextMock(),
-            $this->createDummyConfig(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('Queue name parameter is required but is not set');
-
-        $message = new Message();
-        $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor');
-
-        $driver->sendToProcessor($message);
-    }
-
-    public function testShouldSetupBroker()
-    {
-        $routerTopic = new AmqpTopic('');
-        $routerQueue = new AmqpQueue('');
-
-        $processorQueue = new AmqpQueue('');
-
-        $context = $this->createAmqpContextMock();
-        // setup router
-        $context
-            ->expects($this->at(0))
-            ->method('createTopic')
-            ->willReturn($routerTopic)
-        ;
-        $context
-            ->expects($this->at(1))
-            ->method('createQueue')
-            ->willReturn($routerQueue)
-        ;
-        $context
-            ->expects($this->at(2))
-            ->method('declareTopic')
-            ->with($this->identicalTo($routerTopic))
-        ;
-        $context
-            ->expects($this->at(3))
-            ->method('declareQueue')
-            ->with($this->identicalTo($routerQueue))
-        ;
-        $context
-            ->expects($this->at(4))
-            ->method('bind')
-            ->with($this->isInstanceOf(AmqpBind::class))
-        ;
-        // setup processor queue
-        $context
-            ->expects($this->at(5))
-            ->method('createQueue')
-            ->willReturn($processorQueue)
-        ;
-        $context
-            ->expects($this->at(6))
-            ->method('declareQueue')
-            ->with($this->identicalTo($processorQueue))
-        ;
-
-        $meta = new QueueMetaRegistry($this->createDummyConfig(), [
-            'default' => [],
-        ]);
-
-        $driver = new AmqpDriver(
-            $context,
-            $this->createDummyConfig(),
-            $meta
-        );
-
-        $driver->setupBroker();
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext
-     */
-    private function createAmqpContextMock()
-    {
-        return $this->createMock(AmqpContext::class);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|AmqpProducer
-     */
-    private function createAmqpProducerMock()
-    {
-        return $this->createMock(AmqpProducer::class);
-    }
-
-    /**
-     * @return QueueMetaRegistry
-     */
-    private function createDummyQueueMetaRegistry()
-    {
-        $registry = new QueueMetaRegistry($this->createDummyConfig(), []);
-        $registry->add('default');
-        $registry->add('aFooQueue');
-        $registry->add('aBarQueue', 'aBarQueue');
-
-        return $registry;
-    }
-
-    /**
-     * @return Config
-     */
-    private function createDummyConfig()
-    {
-        return Config::create('aPrefix');
-    }
-}
diff --git a/Tests/Client/Amqp/RabbitMqDriverTest.php b/Tests/Client/Amqp/RabbitMqDriverTest.php
deleted file mode 100644
index 3b67ef5..0000000
--- a/Tests/Client/Amqp/RabbitMqDriverTest.php
+++ /dev/null
@@ -1,599 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Client\Amqp;
-
-use Enqueue\Client\Amqp\AmqpDriver;
-use Enqueue\Client\Amqp\RabbitMqDriver;
-use Enqueue\Client\Config;
-use Enqueue\Client\DriverInterface;
-use Enqueue\Client\Message;
-use Enqueue\Client\MessagePriority;
-use Enqueue\Client\Meta\QueueMetaRegistry;
-use Enqueue\Test\ClassExtensionTrait;
-use Interop\Amqp\AmqpBind;
-use Interop\Amqp\AmqpContext;
-use Interop\Amqp\AmqpProducer;
-use Interop\Amqp\Impl\AmqpMessage;
-use Interop\Amqp\Impl\AmqpQueue;
-use Interop\Amqp\Impl\AmqpTopic;
-use PHPUnit\Framework\TestCase;
-
-class RabbitMqDriverTest extends TestCase
-{
-    use ClassExtensionTrait;
-
-    public function testShouldImplementsDriverInterface()
-    {
-        $this->assertClassImplements(DriverInterface::class, RabbitMqDriver::class);
-    }
-
-    public function testShouldExtendsAmqpDriverClass()
-    {
-        $this->assertClassExtends(AmqpDriver::class, RabbitMqDriver::class);
-    }
-
-    public function testCouldBeConstructedWithRequiredArguments()
-    {
-        new RabbitMqDriver(
-            $this->createAmqpContextMock(),
-            Config::create(),
-            $this->createDummyQueueMetaRegistry()
-        );
-    }
-
-    public function testShouldReturnConfigObject()
-    {
-        $config = Config::create();
-
-        $driver = new RabbitMqDriver($this->createAmqpContextMock(), $config, $this->createDummyQueueMetaRegistry());
-
-        $this->assertSame($config, $driver->getConfig());
-    }
-
-    public function testShouldCreateAndReturnQueueInstance()
-    {
-        $expectedQueue = new AmqpQueue('aName');
-
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createQueue')
-            ->with('aprefix.afooqueue')
-            ->willReturn($expectedQueue)
-        ;
-
-        $driver = new AmqpDriver($context, Config::create(), $this->createDummyQueueMetaRegistry());
-
-        $queue = $driver->createQueue('aFooQueue');
-
-        $this->assertSame($expectedQueue, $queue);
-        $this->assertSame([], $queue->getArguments());
-        $this->assertSame(2, $queue->getFlags());
-        $this->assertNull($queue->getConsumerTag());
-    }
-
-    public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName()
-    {
-        $expectedQueue = new AmqpQueue('aName');
-
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createQueue')
-            ->with('aBarQueue')
-            ->willReturn($expectedQueue)
-        ;
-
-        $driver = new AmqpDriver($context, Config::create(), $this->createDummyQueueMetaRegistry());
-
-        $queue = $driver->createQueue('aBarQueue');
-
-        $this->assertSame($expectedQueue, $queue);
-    }
-
-    public function testShouldConvertTransportMessageToClientMessage()
-    {
-        $transportMessage = new AmqpMessage();
-        $transportMessage->setBody('body');
-        $transportMessage->setHeaders(['hkey' => 'hval']);
-        $transportMessage->setProperties(['key' => 'val']);
-        $transportMessage->setProperty('enqueue-delay', '5678000');
-        $transportMessage->setHeader('content_type', 'ContentType');
-        $transportMessage->setHeader('expiration', '12345000');
-        $transportMessage->setHeader('priority', 3);
-        $transportMessage->setMessageId('MessageId');
-        $transportMessage->setTimestamp(1000);
-        $transportMessage->setReplyTo('theReplyTo');
-        $transportMessage->setCorrelationId('theCorrelationId');
-
-        $driver = new RabbitMqDriver(
-            $this->createAmqpContextMock(),
-            new Config('', '', '', '', '', '', ['delay_strategy' => 'dlx']),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $clientMessage = $driver->createClientMessage($transportMessage);
-
-        $this->assertInstanceOf(Message::class, $clientMessage);
-        $this->assertSame('body', $clientMessage->getBody());
-        $this->assertSame([
-            'hkey' => 'hval',
-            'content_type' => 'ContentType',
-            'expiration' => '12345000',
-            'priority' => 3,
-            'message_id' => 'MessageId',
-            'timestamp' => 1000,
-            'reply_to' => 'theReplyTo',
-            'correlation_id' => 'theCorrelationId',
-        ], $clientMessage->getHeaders());
-        $this->assertSame([
-            'key' => 'val',
-            'enqueue-delay' => '5678000',
-        ], $clientMessage->getProperties());
-        $this->assertSame('MessageId', $clientMessage->getMessageId());
-        $this->assertSame(12345, $clientMessage->getExpire());
-        $this->assertSame(5678, $clientMessage->getDelay());
-        $this->assertSame('ContentType', $clientMessage->getContentType());
-        $this->assertSame(1000, $clientMessage->getTimestamp());
-        $this->assertSame(MessagePriority::HIGH, $clientMessage->getPriority());
-        $this->assertSame('theReplyTo', $clientMessage->getReplyTo());
-        $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId());
-    }
-
-    public function testShouldThrowExceptionIfXDelayIsNotNumeric()
-    {
-        $transportMessage = new AmqpMessage();
-        $transportMessage->setProperty('enqueue-delay', 'is-not-numeric');
-
-        $driver = new RabbitMqDriver(
-            $this->createAmqpContextMock(),
-            Config::create(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('"enqueue-delay" header is not numeric. "is-not-numeric"');
-
-        $driver->createClientMessage($transportMessage);
-    }
-
-    public function testShouldThrowExceptionIfExpirationIsNotNumeric()
-    {
-        $transportMessage = new AmqpMessage();
-        $transportMessage->setHeader('expiration', 'is-not-numeric');
-
-        $driver = new RabbitMqDriver(
-            $this->createAmqpContextMock(),
-            Config::create(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('expiration header is not numeric. "is-not-numeric"');
-
-        $driver->createClientMessage($transportMessage);
-    }
-
-    public function testShouldThrowExceptionIfCantConvertTransportPriorityToClientPriority()
-    {
-        $transportMessage = new AmqpMessage();
-        $transportMessage->setHeader('priority', 'unknown');
-
-        $driver = new RabbitMqDriver(
-            $this->createAmqpContextMock(),
-            Config::create(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('Cant convert transport priority to client: "unknown"');
-
-        $driver->createClientMessage($transportMessage);
-    }
-
-    public function testShouldThrowExceptionIfCantConvertClientPriorityToTransportPriority()
-    {
-        $clientMessage = new Message();
-        $clientMessage->setPriority('unknown');
-
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createMessage')
-            ->willReturn(new AmqpMessage())
-        ;
-
-        $driver = new RabbitMqDriver(
-            $context,
-            Config::create(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('Given priority could not be converted to client\'s one. Got: unknown');
-
-        $driver->createTransportMessage($clientMessage);
-    }
-
-    public function testShouldConvertClientMessageToTransportMessage()
-    {
-        $clientMessage = new Message();
-        $clientMessage->setBody('body');
-        $clientMessage->setHeaders(['hkey' => 'hval']);
-        $clientMessage->setProperties(['key' => 'val']);
-        $clientMessage->setContentType('ContentType');
-        $clientMessage->setExpire(123);
-        $clientMessage->setPriority(MessagePriority::VERY_HIGH);
-        $clientMessage->setDelay(432);
-        $clientMessage->setMessageId('MessageId');
-        $clientMessage->setTimestamp(1000);
-        $clientMessage->setReplyTo('theReplyTo');
-        $clientMessage->setCorrelationId('theCorrelationId');
-
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createMessage')
-            ->willReturn(new AmqpMessage())
-        ;
-
-        $driver = new RabbitMqDriver(
-            $context,
-            new Config('', '', '', '', '', '', ['delay_strategy' => 'dlx']),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $transportMessage = $driver->createTransportMessage($clientMessage);
-
-        $this->assertInstanceOf(AmqpMessage::class, $transportMessage);
-        $this->assertSame('body', $transportMessage->getBody());
-        $this->assertSame([
-            'hkey' => 'hval',
-            'message_id' => 'MessageId',
-            'timestamp' => 1000,
-            'reply_to' => 'theReplyTo',
-            'correlation_id' => 'theCorrelationId',
-            'content_type' => 'ContentType',
-            'delivery_mode' => 2,
-            'expiration' => '123000',
-            'priority' => 4,
-        ], $transportMessage->getHeaders());
-        $this->assertSame([
-            'key' => 'val',
-            'enqueue-delay' => 432000,
-        ], $transportMessage->getProperties());
-        $this->assertSame('MessageId', $transportMessage->getMessageId());
-        $this->assertSame(1000, $transportMessage->getTimestamp());
-        $this->assertSame('theReplyTo', $transportMessage->getReplyTo());
-        $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId());
-    }
-
-    public function testThrowIfDelayNotSupportedOnConvertClientMessageToTransportMessage()
-    {
-        $clientMessage = new Message();
-        $clientMessage->setDelay(432);
-
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createMessage')
-            ->willReturn(new AmqpMessage())
-        ;
-
-        $driver = new RabbitMqDriver(
-            $context,
-            new Config('', '', '', '', '', '', ['delay_strategy' => null]),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The message delaying is not supported. In order to use delay feature install RabbitMQ delay strategy.');
-        $driver->createTransportMessage($clientMessage);
-    }
-
-    public function testShouldSendMessageToRouter()
-    {
-        $topic = new AmqpTopic('');
-        $transportMessage = new AmqpMessage();
-
-        $producer = $this->createAmqpProducerMock();
-        $producer
-            ->expects($this->once())
-            ->method('send')
-            ->with($this->identicalTo($topic), $this->identicalTo($transportMessage))
-        ;
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createTopic')
-            ->willReturn($topic)
-        ;
-        $context
-            ->expects($this->once())
-            ->method('createProducer')
-            ->willReturn($producer)
-        ;
-        $context
-            ->expects($this->once())
-            ->method('createMessage')
-            ->willReturn($transportMessage)
-        ;
-
-        $driver = new RabbitMqDriver(
-            $context,
-            Config::create(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $message = new Message();
-        $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic');
-
-        $driver->sendToRouter($message);
-    }
-
-    public function testShouldThrowExceptionIfTopicParameterIsNotSet()
-    {
-        $driver = new RabbitMqDriver(
-            $this->createAmqpContextMock(),
-            Config::create(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('Topic name parameter is required but is not set');
-
-        $driver->sendToRouter(new Message());
-    }
-
-    public function testShouldSendMessageToProcessor()
-    {
-        $queue = new AmqpQueue('');
-        $transportMessage = new AmqpMessage();
-
-        $producer = $this->createAmqpProducerMock();
-        $producer
-            ->expects($this->once())
-            ->method('send')
-            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
-        ;
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createQueue')
-            ->willReturn($queue)
-        ;
-        $context
-            ->expects($this->once())
-            ->method('createProducer')
-            ->willReturn($producer)
-        ;
-        $context
-            ->expects($this->once())
-            ->method('createMessage')
-            ->willReturn($transportMessage)
-        ;
-
-        $driver = new RabbitMqDriver(
-            $context,
-            Config::create(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $message = new Message();
-        $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor');
-        $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue');
-
-        $driver->sendToProcessor($message);
-    }
-
-    public function testShouldSendMessageToProcessorWithDeliveryDelay()
-    {
-        $queue = new AmqpQueue('');
-        $transportMessage = new AmqpMessage();
-
-        $producer = $this->createAmqpProducerMock();
-        $producer
-            ->expects($this->once())
-            ->method('send')
-            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
-        ;
-        $producer
-            ->expects($this->once())
-            ->method('setDeliveryDelay')
-            ->with($this->identicalTo(10000))
-        ;
-        $context = $this->createAmqpContextMock();
-        $context
-            ->expects($this->once())
-            ->method('createQueue')
-            ->willReturn($queue)
-        ;
-        $context
-            ->expects($this->once())
-            ->method('createProducer')
-            ->willReturn($producer)
-        ;
-        $context
-            ->expects($this->once())
-            ->method('createMessage')
-            ->willReturn($transportMessage)
-        ;
-
-        $driver = new RabbitMqDriver(
-            $context,
-            new Config('', '', '', '', '', '', ['delay_strategy' => 'dlx']),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $message = new Message();
-        $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor');
-        $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue');
-        $message->setDelay(10);
-
-        $driver->sendToProcessor($message);
-    }
-
-    public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet()
-    {
-        $driver = new RabbitMqDriver(
-            $this->createAmqpContextMock(),
-            Config::create(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('Processor name parameter is required but is not set');
-
-        $driver->sendToProcessor(new Message());
-    }
-
-    public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet()
-    {
-        $driver = new RabbitMqDriver(
-            $this->createAmqpContextMock(),
-            Config::create(),
-            $this->createDummyQueueMetaRegistry()
-        );
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('Queue name parameter is required but is not set');
-
-        $message = new Message();
-        $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor');
-
-        $driver->sendToProcessor($message);
-    }
-
-    public function testShouldSetupBrokerWhenDelayPluginNotInstalled()
-    {
-        $routerTopic = new AmqpTopic('');
-        $routerQueue = new AmqpQueue('');
-
-        $processorQueue = new AmqpQueue('');
-
-        $context = $this->createAmqpContextMock();
-        // setup router
-        $context
-            ->expects($this->at(0))
-            ->method('createTopic')
-            ->willReturn($routerTopic)
-        ;
-        $context
-            ->expects($this->at(1))
-            ->method('createQueue')
-            ->willReturn($routerQueue)
-        ;
-        $context
-            ->expects($this->at(2))
-            ->method('declareTopic')
-            ->with($this->identicalTo($routerTopic))
-        ;
-        $context
-            ->expects($this->at(3))
-            ->method('declareQueue')
-            ->with($this->identicalTo($routerQueue))
-        ;
-        $context
-            ->expects($this->at(4))
-            ->method('bind')
-            ->with($this->isInstanceOf(AmqpBind::class))
-        ;
-        // setup processor queue
-        $context
-            ->expects($this->at(5))
-            ->method('createQueue')
-            ->willReturn($processorQueue)
-        ;
-
-        $config = Config::create('', '', '', '', '', '', ['delay_strategy' => null]);
-
-        $meta = new QueueMetaRegistry($config, ['default' => []]);
-
-        $driver = new RabbitMqDriver($context, $config, $meta);
-
-        $driver->setupBroker();
-    }
-
-    public function testShouldSetupBroker()
-    {
-        $routerTopic = new AmqpTopic('');
-        $routerQueue = new AmqpQueue('');
-
-        $processorQueue = new AmqpQueue('');
-
-        $context = $this->createAmqpContextMock();
-        // setup router
-        $context
-            ->expects($this->at(0))
-            ->method('createTopic')
-            ->willReturn($routerTopic)
-        ;
-        $context
-            ->expects($this->at(1))
-            ->method('createQueue')
-            ->willReturn($routerQueue)
-        ;
-        $context
-            ->expects($this->at(2))
-            ->method('declareTopic')
-            ->with($this->identicalTo($routerTopic))
-        ;
-        $context
-            ->expects($this->at(3))
-            ->method('declareQueue')
-            ->with($this->identicalTo($routerQueue))
-        ;
-        $context
-            ->expects($this->at(4))
-            ->method('bind')
-            ->with($this->isInstanceOf(AmqpBind::class))
-        ;
-        // setup processor queue
-        $context
-            ->expects($this->at(5))
-            ->method('createQueue')
-            ->willReturn($processorQueue)
-        ;
-        $context
-            ->expects($this->at(6))
-            ->method('declareQueue')
-            ->with($this->identicalTo($processorQueue))
-        ;
-
-        $config = Config::create('', '', '', '', '', '', ['delay_strategy' => 'dlx']);
-
-        $meta = new QueueMetaRegistry($config, ['default' => []]);
-
-        $driver = new RabbitMqDriver($context, $config, $meta);
-
-        $driver->setupBroker();
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext
-     */
-    private function createAmqpContextMock()
-    {
-        return $this->createMock(AmqpContext::class);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|AmqpProducer
-     */
-    private function createAmqpProducerMock()
-    {
-        return $this->createMock(AmqpProducer::class);
-    }
-
-    /**
-     * @return QueueMetaRegistry
-     */
-    private function createDummyQueueMetaRegistry()
-    {
-        $registry = new QueueMetaRegistry(Config::create('aPrefix'), []);
-        $registry->add('aFooQueue');
-        $registry->add('aBarQueue', 'aBarQueue');
-
-        return $registry;
-    }
-}
diff --git a/Tests/Client/ChainExtensionTest.php b/Tests/Client/ChainExtensionTest.php
index 3b1d82f..0f42bcf 100644
--- a/Tests/Client/ChainExtensionTest.php
+++ b/Tests/Client/ChainExtensionTest.php
@@ -3,9 +3,16 @@
 namespace Enqueue\Tests\Client;
 
 use Enqueue\Client\ChainExtension;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\DriverPreSend;
 use Enqueue\Client\ExtensionInterface;
 use Enqueue\Client\Message;
+use Enqueue\Client\PostSend;
+use Enqueue\Client\PreSend;
+use Enqueue\Client\ProducerInterface;
 use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as TransportMessage;
 use PHPUnit\Framework\TestCase;
 
 class ChainExtensionTest extends TestCase
@@ -17,57 +24,129 @@ public function testShouldImplementExtensionInterface()
         $this->assertClassImplements(ExtensionInterface::class, ChainExtension::class);
     }
 
-    public function testCouldBeConstructedWithExtensionsArray()
+    public function testShouldBeFinal()
     {
-        new ChainExtension([$this->createExtension(), $this->createExtension()]);
+        $this->assertClassFinal(ChainExtension::class);
     }
 
-    public function testShouldProxyOnPreSendToAllInternalExtensions()
+    public function testThrowIfArrayContainsNotExtension()
     {
-        $message = new Message();
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Invalid extension given');
+
+        new ChainExtension([$this->createExtension(), new \stdClass()]);
+    }
+
+    public function testShouldProxyOnPreSendEventToAllInternalExtensions()
+    {
+        $preSend = new PreSend(
+            'aCommandOrTopic',
+            new Message(),
+            $this->createMock(ProducerInterface::class),
+            $this->createMock(DriverInterface::class)
+        );
+
+        $fooExtension = $this->createExtension();
+        $fooExtension
+            ->expects($this->once())
+            ->method('onPreSendEvent')
+            ->with($this->identicalTo($preSend))
+        ;
+        $barExtension = $this->createExtension();
+        $barExtension
+            ->expects($this->once())
+            ->method('onPreSendEvent')
+            ->with($this->identicalTo($preSend))
+        ;
+
+        $extensions = new ChainExtension([$fooExtension, $barExtension]);
+
+        $extensions->onPreSendEvent($preSend);
+    }
+
+    public function testShouldProxyOnPreSendCommandToAllInternalExtensions()
+    {
+        $preSend = new PreSend(
+            'aCommandOrTopic',
+            new Message(),
+            $this->createMock(ProducerInterface::class),
+            $this->createMock(DriverInterface::class)
+        );
+
+        $fooExtension = $this->createExtension();
+        $fooExtension
+            ->expects($this->once())
+            ->method('onPreSendCommand')
+            ->with($this->identicalTo($preSend))
+        ;
+        $barExtension = $this->createExtension();
+        $barExtension
+            ->expects($this->once())
+            ->method('onPreSendCommand')
+            ->with($this->identicalTo($preSend))
+        ;
+
+        $extensions = new ChainExtension([$fooExtension, $barExtension]);
+
+        $extensions->onPreSendCommand($preSend);
+    }
+
+    public function testShouldProxyOnDriverPreSendToAllInternalExtensions()
+    {
+        $driverPreSend = new DriverPreSend(
+            new Message(),
+            $this->createMock(ProducerInterface::class),
+            $this->createMock(DriverInterface::class)
+        );
 
         $fooExtension = $this->createExtension();
         $fooExtension
             ->expects($this->once())
-            ->method('onPreSend')
-            ->with('topic', $this->identicalTo($message))
+            ->method('onDriverPreSend')
+            ->with($this->identicalTo($driverPreSend))
         ;
         $barExtension = $this->createExtension();
         $barExtension
             ->expects($this->once())
-            ->method('onPreSend')
-            ->with('topic', $this->identicalTo($message))
+            ->method('onDriverPreSend')
+            ->with($this->identicalTo($driverPreSend))
         ;
 
         $extensions = new ChainExtension([$fooExtension, $barExtension]);
 
-        $extensions->onPreSend('topic', $message);
+        $extensions->onDriverPreSend($driverPreSend);
     }
 
-    public function testShouldProxyOnPostSendToAllInternalExtensions()
+    public function testShouldProxyOnPostSentToAllInternalExtensions()
     {
-        $message = new Message();
+        $postSend = new PostSend(
+            new Message(),
+            $this->createMock(ProducerInterface::class),
+            $this->createMock(DriverInterface::class),
+            $this->createMock(Destination::class),
+            $this->createMock(TransportMessage::class)
+        );
 
         $fooExtension = $this->createExtension();
         $fooExtension
             ->expects($this->once())
             ->method('onPostSend')
-            ->with('topic', $this->identicalTo($message))
+            ->with($this->identicalTo($postSend))
         ;
         $barExtension = $this->createExtension();
         $barExtension
             ->expects($this->once())
             ->method('onPostSend')
-            ->with('topic', $this->identicalTo($message))
+            ->with($this->identicalTo($postSend))
         ;
 
         $extensions = new ChainExtension([$fooExtension, $barExtension]);
 
-        $extensions->onPostSend('topic', $message);
+        $extensions->onPostSend($postSend);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|ExtensionInterface
+     * @return \PHPUnit\Framework\MockObject\MockObject|ExtensionInterface
      */
     protected function createExtension()
     {
diff --git a/Tests/Client/ConfigTest.php b/Tests/Client/ConfigTest.php
index 1ecb0f9..09b80e2 100644
--- a/Tests/Client/ConfigTest.php
+++ b/Tests/Client/ConfigTest.php
@@ -7,139 +7,246 @@
 
 class ConfigTest extends TestCase
 {
-    public function testShouldReturnRouterProcessorNameSetInConstructor()
+    public function testShouldReturnPrefixSetInConstructor()
     {
         $config = new Config(
-            'aPrefix',
+            'thePrefix',
+            'theSeparator',
             'aApp',
             'aRouterTopicName',
             'aRouterQueueName',
             'aDefaultQueueName',
-            'aRouterProcessorName'
+            'aRouterProcessorName',
+            [],
+            []
         );
 
-        $this->assertEquals('aRouterProcessorName', $config->getRouterProcessorName());
+        $this->assertEquals('thePrefix', $config->getPrefix());
     }
 
-    public function testShouldReturnRouterTopicNameSetInConstructor()
+    /**
+     * @dataProvider provideEmptyStrings
+     */
+    public function testShouldTrimReturnPrefixSetInConstructor(string $empty)
     {
         $config = new Config(
-            'aPrefix',
+            $empty,
             'aApp',
+            'theSeparator',
             'aRouterTopicName',
             'aRouterQueueName',
             'aDefaultQueueName',
-            'aRouterProcessorName'
+            'aRouterProcessorName',
+            [],
+            []
         );
 
-        $this->assertEquals('aRouterTopicName', $config->getRouterTopicName());
+        $this->assertSame('', $config->getPrefix());
     }
 
-    public function testShouldReturnRouterQueueNameSetInConstructor()
+    public function testShouldReturnAppNameSetInConstructor()
     {
         $config = new Config(
             'aPrefix',
-            'aApp',
+            'theSeparator',
+            'theApp',
             'aRouterTopicName',
             'aRouterQueueName',
             'aDefaultQueueName',
-            'aRouterProcessorName'
+            'aRouterProcessorName',
+            [],
+            []
         );
 
-        $this->assertEquals('aRouterQueueName', $config->getRouterQueueName());
+        $this->assertEquals('theApp', $config->getApp());
     }
 
-    public function testShouldReturnDefaultQueueNameSetInConstructor()
+    /**
+     * @dataProvider provideEmptyStrings
+     */
+    public function testShouldTrimReturnAppNameSetInConstructor(string $empty)
     {
         $config = new Config(
             'aPrefix',
-            'aApp',
+            'theSeparator',
+            $empty,
             'aRouterTopicName',
             'aRouterQueueName',
             'aDefaultQueueName',
-            'aRouterProcessorName'
+            'aRouterProcessorName',
+            [],
+            []
         );
 
-        $this->assertEquals('aDefaultQueueName', $config->getDefaultProcessorQueueName());
+        $this->assertSame('', $config->getApp());
     }
 
-    public function testShouldCreateRouterTopicName()
+    public function testShouldReturnRouterProcessorNameSetInConstructor()
     {
         $config = new Config(
             'aPrefix',
+            'theSeparator',
             'aApp',
             'aRouterTopicName',
             'aRouterQueueName',
             'aDefaultQueueName',
-            'aRouterProcessorName'
+            'aRouterProcessorName',
+            [],
+            []
         );
 
-        $this->assertEquals('aprefix.aname', $config->createTransportRouterTopicName('aName'));
+        $this->assertEquals('aRouterProcessorName', $config->getRouterProcessor());
     }
 
-    public function testShouldCreateProcessorQueueName()
+    public function testShouldReturnRouterTopicNameSetInConstructor()
     {
         $config = new Config(
             'aPrefix',
+            'theSeparator',
             'aApp',
             'aRouterTopicName',
             'aRouterQueueName',
             'aDefaultQueueName',
-            'aRouterProcessorName'
+            'aRouterProcessorName',
+            [],
+            []
         );
 
-        $this->assertEquals('aprefix.aapp.aname', $config->createTransportQueueName('aName'));
+        $this->assertEquals('aRouterTopicName', $config->getRouterTopic());
     }
 
-    public function testShouldCreateProcessorQueueNameWithoutAppName()
+    public function testShouldReturnRouterQueueNameSetInConstructor()
     {
         $config = new Config(
             'aPrefix',
-            '',
+            'theSeparator',
+            'aApp',
             'aRouterTopicName',
             'aRouterQueueName',
             'aDefaultQueueName',
-            'aRouterProcessorName'
+            'aRouterProcessorName',
+            [],
+            []
         );
 
-        $this->assertEquals('aprefix.aname', $config->createTransportQueueName('aName'));
+        $this->assertEquals('aRouterQueueName', $config->getRouterQueue());
     }
 
-    public function testShouldCreateProcessorQueueNameWithoutPrefix()
+    public function testShouldReturnDefaultQueueNameSetInConstructor()
     {
         $config = new Config(
-            '',
+            'aPrefix',
+            'theSeparator',
             'aApp',
             'aRouterTopicName',
             'aRouterQueueName',
             'aDefaultQueueName',
-            'aRouterProcessorName'
+            'aRouterProcessorName',
+            [],
+            []
         );
 
-        $this->assertEquals('aapp.aname', $config->createTransportQueueName('aName'));
+        $this->assertEquals('aDefaultQueueName', $config->getDefaultQueue());
     }
 
-    public function testShouldCreateProcessorQueueNameWithoutPrefixAndAppName()
+    public function testShouldCreateDefaultConfig()
     {
-        $config = new Config(
+        $config = Config::create();
+
+        $this->assertSame('default', $config->getDefaultQueue());
+        $this->assertSame('router', $config->getRouterProcessor());
+        $this->assertSame('default', $config->getRouterQueue());
+        $this->assertSame('router', $config->getRouterTopic());
+    }
+
+    /**
+     * @dataProvider provideEmptyStrings
+     */
+    public function testThrowIfRouterTopicNameIsEmpty(string $empty)
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Router topic is empty.');
+        new Config(
             '',
             '',
-            'aRouterTopicName',
+            '',
+            $empty,
             'aRouterQueueName',
             'aDefaultQueueName',
-            'aRouterProcessorName'
+            'aRouterProcessorName',
+            [],
+            []
+        );
+    }
+
+    /**
+     * @dataProvider provideEmptyStrings
+     */
+    public function testThrowIfRouterQueueNameIsEmpty(string $empty)
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Router queue is empty.');
+        new Config(
+            '',
+            '',
+            '',
+            'aRouterTopicName',
+            $empty,
+            'aDefaultQueueName',
+            'aRouterProcessorName',
+            [],
+            []
+        );
+    }
+
+    /**
+     * @dataProvider provideEmptyStrings
+     */
+    public function testThrowIfDefaultQueueNameIsEmpty(string $empty)
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Default processor queue name is empty.');
+        new Config(
+            '',
+            '',
+            '',
+            'aRouterTopicName',
+            'aRouterQueueName',
+            $empty,
+            'aRouterProcessorName',
+            [],
+            []
         );
+    }
 
-        $this->assertEquals('aname', $config->createTransportQueueName('aName'));
+    /**
+     * @dataProvider provideEmptyStrings
+     */
+    public function testThrowIfRouterProcessorNameIsEmpty(string $empty)
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Router processor name is empty.');
+        new Config(
+            '',
+            '',
+            '',
+            'aRouterTopicName',
+            'aRouterQueueName',
+            'aDefaultQueueName',
+            $empty,
+            [],
+            []
+        );
     }
 
-    public function testShouldCreateDefaultConfig()
+    public function provideEmptyStrings()
     {
-        $config = Config::create();
+        yield [''];
+
+        yield [' '];
+
+        yield ['  '];
 
-        $this->assertSame('default', $config->getDefaultProcessorQueueName());
-        $this->assertSame('router', $config->getRouterProcessorName());
-        $this->assertSame('default', $config->getRouterQueueName());
-        $this->assertSame('router', $config->getRouterTopicName());
+        yield ["\t"];
     }
 }
diff --git a/Tests/Client/ConsumptionExtension/DelayRedeliveredMessageExtensionTest.php b/Tests/Client/ConsumptionExtension/DelayRedeliveredMessageExtensionTest.php
index b61a341..a660126 100644
--- a/Tests/Client/ConsumptionExtension/DelayRedeliveredMessageExtensionTest.php
+++ b/Tests/Client/ConsumptionExtension/DelayRedeliveredMessageExtensionTest.php
@@ -4,22 +4,25 @@
 
 use Enqueue\Client\ConsumptionExtension\DelayRedeliveredMessageExtension;
 use Enqueue\Client\DriverInterface;
+use Enqueue\Client\DriverSendResult;
 use Enqueue\Client\Message;
-use Enqueue\Consumption\Context;
+use Enqueue\Consumption\Context\MessageReceived;
 use Enqueue\Consumption\Result;
 use Enqueue\Null\NullMessage;
 use Enqueue\Null\NullQueue;
-use Interop\Queue\PsrContext;
+use Enqueue\Test\TestLogger;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context as InteropContext;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as TransportMessage;
+use Interop\Queue\Processor;
+use Interop\Queue\Queue;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
-use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 
 class DelayRedeliveredMessageExtensionTest extends TestCase
 {
-    public function testCouldBeConstructedWithRequiredArguments()
-    {
-        new DelayRedeliveredMessageExtension($this->createDriverMock(), 12345);
-    }
-
     public function testShouldSendDelayedMessageAndRejectOriginalMessage()
     {
         $queue = new NullQueue('queue');
@@ -38,6 +41,7 @@ public function testShouldSendDelayedMessageAndRejectOriginalMessage()
             ->expects(self::once())
             ->method('sendToProcessor')
             ->with(self::isInstanceOf(Message::class))
+            ->willReturn($this->createDriverSendResult())
         ;
         $driver
             ->expects(self::once())
@@ -46,32 +50,23 @@ public function testShouldSendDelayedMessageAndRejectOriginalMessage()
             ->willReturn($delayedMessage)
         ;
 
-        $logger = $this->createLoggerMock();
-        $logger
-            ->expects(self::at(0))
-            ->method('debug')
-            ->with('[DelayRedeliveredMessageExtension] Send delayed message')
-        ;
-        $logger
-            ->expects(self::at(1))
-            ->method('debug')
-            ->with(
-                '[DelayRedeliveredMessageExtension] '.
-                'Reject redelivered original message by setting reject status to context.'
-            )
-        ;
+        $logger = new TestLogger();
 
-        $context = new Context($this->createPsrContextMock());
-        $context->setPsrQueue($queue);
-        $context->setPsrMessage($originMessage);
-        $context->setLogger($logger);
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub($queue),
+            $originMessage,
+            $this->createProcessorMock(),
+            1,
+            $logger
+        );
 
-        $this->assertNull($context->getResult());
+        $this->assertNull($messageReceived->getResult());
 
         $extension = new DelayRedeliveredMessageExtension($driver, 12345);
-        $extension->onPreReceived($context);
+        $extension->onMessageReceived($messageReceived);
 
-        $result = $context->getResult();
+        $result = $messageReceived->getResult();
         $this->assertInstanceOf(Result::class, $result);
         $this->assertSame(Result::REJECT, $result->getStatus());
         $this->assertSame('A new copy of the message was sent with a delay. The original message is rejected', $result->getReason());
@@ -80,6 +75,16 @@ public function testShouldSendDelayedMessageAndRejectOriginalMessage()
         $this->assertEquals([
             'enqueue.redelivery_count' => 1,
         ], $delayedMessage->getProperties());
+
+        self::assertTrue(
+            $logger->hasDebugThatContains('[DelayRedeliveredMessageExtension] Send delayed message')
+        );
+        self::assertTrue(
+            $logger->hasDebugThatContains(
+                '[DelayRedeliveredMessageExtension] '.
+                'Reject redelivered original message by setting reject status to context.'
+            )
+        );
     }
 
     public function testShouldDoNothingIfMessageIsNotRedelivered()
@@ -92,13 +97,19 @@ public function testShouldDoNothingIfMessageIsNotRedelivered()
             ->method('sendToProcessor')
         ;
 
-        $context = new Context($this->createPsrContextMock());
-        $context->setPsrMessage($message);
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub(null),
+            $message,
+            $this->createProcessorMock(),
+            1,
+            new NullLogger()
+        );
 
         $extension = new DelayRedeliveredMessageExtension($driver, 12345);
-        $extension->onPreReceived($context);
+        $extension->onMessageReceived($messageReceived);
 
-        $this->assertNull($context->getResult());
+        $this->assertNull($messageReceived->getResult());
     }
 
     public function testShouldDoNothingIfMessageIsRedeliveredButResultWasAlreadySetOnContext()
@@ -112,35 +123,64 @@ public function testShouldDoNothingIfMessageIsRedeliveredButResultWasAlreadySetO
             ->method('sendToProcessor')
         ;
 
-        $context = new Context($this->createPsrContextMock());
-        $context->setPsrMessage($message);
-        $context->setResult('aStatus');
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub(null),
+            $message,
+            $this->createProcessorMock(),
+            1,
+            new NullLogger()
+        );
+        $messageReceived->setResult(Result::ack());
 
         $extension = new DelayRedeliveredMessageExtension($driver, 12345);
-        $extension->onPreReceived($context);
+        $extension->onMessageReceived($messageReceived);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface
+     * @return MockObject
      */
-    private function createDriverMock()
+    private function createDriverMock(): DriverInterface
     {
         return $this->createMock(DriverInterface::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext
+     * @return MockObject
+     */
+    private function createContextMock(): InteropContext
+    {
+        return $this->createMock(InteropContext::class);
+    }
+
+    /**
+     * @return MockObject
      */
-    private function createPsrContextMock()
+    private function createProcessorMock(): Processor
     {
-        return $this->createMock(PsrContext::class);
+        return $this->createMock(Processor::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|LoggerInterface
+     * @return MockObject|Consumer
      */
-    private function createLoggerMock()
+    private function createConsumerStub(?Queue $queue): Consumer
+    {
+        $consumerMock = $this->createMock(Consumer::class);
+        $consumerMock
+            ->expects($this->any())
+            ->method('getQueue')
+            ->willReturn($queue ?? new NullQueue('queue'))
+        ;
+
+        return $consumerMock;
+    }
+
+    private function createDriverSendResult(): DriverSendResult
     {
-        return $this->createMock(LoggerInterface::class);
+        return new DriverSendResult(
+            $this->createMock(Destination::class),
+            $this->createMock(TransportMessage::class)
+        );
     }
 }
diff --git a/Tests/Client/ConsumptionExtension/ExclusiveCommandExtensionTest.php b/Tests/Client/ConsumptionExtension/ExclusiveCommandExtensionTest.php
index acbcb41..b1e47c8 100644
--- a/Tests/Client/ConsumptionExtension/ExclusiveCommandExtensionTest.php
+++ b/Tests/Client/ConsumptionExtension/ExclusiveCommandExtensionTest.php
@@ -4,14 +4,19 @@
 
 use Enqueue\Client\Config;
 use Enqueue\Client\ConsumptionExtension\ExclusiveCommandExtension;
-use Enqueue\Client\ExtensionInterface as ClientExtensionInterface;
-use Enqueue\Client\Message;
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\ExtensionInterface as ConsumptionExtensionInterface;
-use Enqueue\Null\NullContext;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\MessageReceivedExtensionInterface;
 use Enqueue\Null\NullMessage;
 use Enqueue\Null\NullQueue;
 use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context as InteropContext;
+use Interop\Queue\Processor;
+use Interop\Queue\Queue;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Psr\Log\NullLogger;
 
@@ -19,176 +24,263 @@ class ExclusiveCommandExtensionTest extends TestCase
 {
     use ClassExtensionTrait;
 
-    public function testShouldImplementConsumptionExtensionInterface()
+    public function testShouldImplementMessageReceivedExtensionInterface()
     {
-        $this->assertClassImplements(ConsumptionExtensionInterface::class, ExclusiveCommandExtension::class);
+        $this->assertClassImplements(MessageReceivedExtensionInterface::class, ExclusiveCommandExtension::class);
     }
 
-    public function testShouldImplementClientExtensionInterface()
+    public function testShouldBeFinal()
     {
-        $this->assertClassImplements(ClientExtensionInterface::class, ExclusiveCommandExtension::class);
-    }
-
-    public function testCouldBeConstructedWithQueueNameToProcessorNameMap()
-    {
-        new ExclusiveCommandExtension([]);
-
-        new ExclusiveCommandExtension(['fooQueueName' => 'fooProcessorName']);
+        $this->assertClassFinal(ExclusiveCommandExtension::class);
     }
 
     public function testShouldDoNothingIfMessageHasTopicPropertySetOnPreReceive()
     {
         $message = new NullMessage();
-        $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'aTopic');
+        $message->setProperty(Config::TOPIC, 'aTopic');
 
-        $context = new Context(new NullContext());
-        $context->setPsrMessage($message);
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->never())
+            ->method('createQueue')
+        ;
 
-        $extension = new ExclusiveCommandExtension([
-            'aFooQueueName' => 'aFooProcessorName',
-        ]);
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub(null),
+            $message,
+            $this->createProcessorMock(),
+            1,
+            new NullLogger()
+        );
+
+        $extension = new ExclusiveCommandExtension($driver);
 
-        $extension->onPreReceived($context);
+        $extension->onMessageReceived($messageReceived);
 
-        self::assertNull($context->getResult());
+        self::assertNull($messageReceived->getResult());
 
         $this->assertEquals([
-            'enqueue.topic_name' => 'aTopic',
+            Config::TOPIC => 'aTopic',
         ], $message->getProperties());
     }
 
-    public function testShouldDoNothingIfMessageHasProcessorNamePropertySetOnPreReceive()
+    public function testShouldDoNothingIfMessageHasCommandPropertySetOnPreReceive()
     {
         $message = new NullMessage();
-        $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'aProcessor');
+        $message->setProperty(Config::COMMAND, 'aCommand');
 
-        $context = new Context(new NullContext());
-        $context->setPsrMessage($message);
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub(null),
+            $message,
+            $this->createProcessorMock(),
+            1,
+            new NullLogger()
+        );
 
-        $extension = new ExclusiveCommandExtension([
-            'aFooQueueName' => 'aFooProcessorName',
-        ]);
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->never())
+            ->method('createQueue')
+        ;
+
+        $extension = new ExclusiveCommandExtension($driver);
 
-        $extension->onPreReceived($context);
+        $extension->onMessageReceived($messageReceived);
 
-        self::assertNull($context->getResult());
+        self::assertNull($messageReceived->getResult());
 
         $this->assertEquals([
-            'enqueue.processor_name' => 'aProcessor',
+            Config::COMMAND => 'aCommand',
         ], $message->getProperties());
     }
 
-    public function testShouldDoNothingIfMessageHasProcessorQueueNamePropertySetOnPreReceive()
+    public function testShouldDoNothingIfMessageHasProcessorPropertySetOnPreReceive()
     {
         $message = new NullMessage();
-        $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aProcessorQueueName');
+        $message->setProperty(Config::PROCESSOR, 'aProcessor');
 
-        $context = new Context(new NullContext());
-        $context->setPsrMessage($message);
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub(null),
+            $message,
+            $this->createProcessorMock(),
+            1,
+            new NullLogger()
+        );
 
-        $extension = new ExclusiveCommandExtension([
-            'aFooQueueName' => 'aFooProcessorName',
-        ]);
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->never())
+            ->method('createQueue')
+        ;
+
+        $extension = new ExclusiveCommandExtension($driver);
 
-        $extension->onPreReceived($context);
+        $extension->onMessageReceived($messageReceived);
 
-        self::assertNull($context->getResult());
+        self::assertNull($messageReceived->getResult());
 
         $this->assertEquals([
-            'enqueue.processor_queue_name' => 'aProcessorQueueName',
+            Config::PROCESSOR => 'aProcessor',
         ], $message->getProperties());
     }
 
-    public function testShouldDoNothingIfCurrentQueueIsNotInTheMap()
+    public function testShouldDoNothingIfCurrentQueueHasNoExclusiveProcessor()
     {
         $message = new NullMessage();
         $queue = new NullQueue('aBarQueueName');
 
-        $context = new Context(new NullContext());
-        $context->setPsrMessage($message);
-        $context->setPsrQueue($queue);
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub($queue),
+            $message,
+            $this->createProcessorMock(),
+            1,
+            new NullLogger()
+        );
 
-        $extension = new ExclusiveCommandExtension([
-            'aFooQueueName' => 'aFooProcessorName',
-        ]);
+        $extension = new ExclusiveCommandExtension($this->createDriverStub(new RouteCollection([])));
 
-        $extension->onPreReceived($context);
+        $extension->onMessageReceived($messageReceived);
 
-        self::assertNull($context->getResult());
+        self::assertNull($messageReceived->getResult());
 
         $this->assertEquals([], $message->getProperties());
     }
 
-    public function testShouldSetCommandPropertiesIfCurrentQueueInTheMap()
+    public function testShouldSetCommandPropertiesIfCurrentQueueHasExclusiveCommandProcessor()
     {
         $message = new NullMessage();
-        $queue = new NullQueue('aFooQueueName');
-
-        $context = new Context(new NullContext());
-        $context->setPsrMessage($message);
-        $context->setPsrQueue($queue);
-        $context->setLogger(new NullLogger());
-
-        $extension = new ExclusiveCommandExtension([
-            'aFooQueueName' => 'aFooProcessorName',
+        $queue = new NullQueue('fooQueue');
+
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub($queue),
+            $message,
+            $this->createProcessorMock(),
+            1,
+            new NullLogger()
+        );
+
+        $routeCollection = new RouteCollection([
+            new Route('fooCommand', Route::COMMAND, 'theFooProcessor', [
+                'exclusive' => true,
+                'queue' => 'fooQueue',
+            ]),
+            new Route('barCommand', Route::COMMAND, 'theFooProcessor', [
+                'exclusive' => true,
+                'queue' => 'barQueue',
+            ]),
         ]);
 
-        $extension->onPreReceived($context);
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->any())
+            ->method('createRouteQueue')
+            ->with($this->isInstanceOf(Route::class))
+            ->willReturnCallback(function (Route $route) {
+                return new NullQueue($route->getQueue());
+            })
+        ;
+
+        $extension = new ExclusiveCommandExtension($driver);
+        $extension->onMessageReceived($messageReceived);
 
-        self::assertNull($context->getResult());
+        self::assertNull($messageReceived->getResult());
 
         $this->assertEquals([
-            'enqueue.topic_name' => '__command__',
-            'enqueue.processor_queue_name' => 'aFooQueueName',
-            'enqueue.processor_name' => 'aFooProcessorName',
-            'enqueue.command_name' => 'aFooProcessorName',
+            Config::PROCESSOR => 'theFooProcessor',
+            Config::COMMAND => 'fooCommand',
         ], $message->getProperties());
     }
 
-    public function testShouldDoNothingOnPreSendIfTopicNotCommandOne()
+    public function testShouldDoNothingIfAnotherQueue()
     {
-        $message = new Message();
-
-        $extension = new ExclusiveCommandExtension([
-            'aFooQueueName' => 'aFooProcessorName',
+        $message = new NullMessage();
+        $queue = new NullQueue('barQueue');
+
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub($queue),
+            $message,
+            $this->createProcessorMock(),
+            1,
+            new NullLogger()
+        );
+
+        $routeCollection = new RouteCollection([
+            new Route('fooCommand', Route::COMMAND, 'theFooProcessor', [
+                'exclusive' => true,
+                'queue' => 'fooQueue',
+            ]),
+            new Route('barCommand', Route::COMMAND, 'theFooProcessor', [
+                'exclusive' => false,
+                'queue' => 'barQueue',
+            ]),
         ]);
 
-        $extension->onPreSend('aTopic', $message);
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->any())
+            ->method('createQueue')
+            ->willReturnCallback(function (string $queueName) {
+                return new NullQueue($queueName);
+            })
+        ;
+
+        $extension = new ExclusiveCommandExtension($driver);
+        $extension->onMessageReceived($messageReceived);
+
+        self::assertNull($messageReceived->getResult());
 
         $this->assertEquals([], $message->getProperties());
     }
 
-    public function testShouldDoNothingIfCommandNotExclusive()
+    /**
+     * @return MockObject|DriverInterface
+     */
+    private function createDriverStub(?RouteCollection $routeCollection = null): DriverInterface
     {
-        $message = new Message();
-        $message->setProperty(Config::PARAMETER_COMMAND_NAME, 'theBarProcessorName');
-
-        $extension = new ExclusiveCommandExtension([
-            'aFooQueueName' => 'aFooProcessorName',
-        ]);
-
-        $extension->onPreSend(Config::COMMAND_TOPIC, $message);
-
-        $this->assertEquals([
-            'enqueue.command_name' => 'theBarProcessorName',
-        ], $message->getProperties());
+        $driver = $this->createMock(DriverInterface::class);
+        $driver
+            ->expects($this->any())
+            ->method('getRouteCollection')
+            ->willReturn($routeCollection ?? new RouteCollection([]))
+        ;
+
+        return $driver;
     }
 
-    public function testShouldForceExclusiveCommandQueue()
+    /**
+     * @return MockObject
+     */
+    private function createContextMock(): InteropContext
     {
-        $message = new Message();
-        $message->setProperty(Config::PARAMETER_COMMAND_NAME, 'aFooProcessorName');
-
-        $extension = new ExclusiveCommandExtension([
-            'aFooQueueName' => 'aFooProcessorName',
-        ]);
+        return $this->createMock(InteropContext::class);
+    }
 
-        $extension->onPreSend(Config::COMMAND_TOPIC, $message);
+    /**
+     * @return MockObject
+     */
+    private function createProcessorMock(): Processor
+    {
+        return $this->createMock(Processor::class);
+    }
 
-        $this->assertEquals([
-            'enqueue.command_name' => 'aFooProcessorName',
-            'enqueue.processor_name' => 'aFooProcessorName',
-            'enqueue.processor_queue_name' => 'aFooQueueName',
-        ], $message->getProperties());
+    /**
+     * @return MockObject|Consumer
+     */
+    private function createConsumerStub(?Queue $queue): Consumer
+    {
+        $consumerMock = $this->createMock(Consumer::class);
+        $consumerMock
+            ->expects($this->any())
+            ->method('getQueue')
+            ->willReturn($queue ?? new NullQueue('queue'))
+        ;
+
+        return $consumerMock;
     }
 }
diff --git a/Tests/Client/ConsumptionExtension/FlushSpoolProducerExtensionTest.php b/Tests/Client/ConsumptionExtension/FlushSpoolProducerExtensionTest.php
index 1469c99..6a782c5 100644
--- a/Tests/Client/ConsumptionExtension/FlushSpoolProducerExtensionTest.php
+++ b/Tests/Client/ConsumptionExtension/FlushSpoolProducerExtensionTest.php
@@ -4,86 +4,33 @@
 
 use Enqueue\Client\ConsumptionExtension\FlushSpoolProducerExtension;
 use Enqueue\Client\SpoolProducer;
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\Context\End;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\EndExtensionInterface;
+use Enqueue\Consumption\PostMessageReceivedExtensionInterface;
 use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Message;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
+use Psr\Log\NullLogger;
 
 class FlushSpoolProducerExtensionTest extends TestCase
 {
     use ClassExtensionTrait;
 
-    public function testShouldImplementExtensionInterface()
+    public function testShouldImplementPostMessageReceivedExtensionInterface()
     {
-        $this->assertClassImplements(ExtensionInterface::class, FlushSpoolProducerExtension::class);
+        $this->assertClassImplements(PostMessageReceivedExtensionInterface::class, FlushSpoolProducerExtension::class);
     }
 
-    public function testCouldBeConstructedWithSpoolProducerAsFirstArgument()
+    public function testShouldImplementEndExtensionInterface()
     {
-        new FlushSpoolProducerExtension($this->createSpoolProducerMock());
+        $this->assertClassImplements(EndExtensionInterface::class, FlushSpoolProducerExtension::class);
     }
 
-    public function testShouldDoNothingOnStart()
-    {
-        $producer = $this->createSpoolProducerMock();
-        $producer
-            ->expects(self::never())
-            ->method('flush')
-        ;
-
-        $extension = new FlushSpoolProducerExtension($producer);
-        $extension->onStart($this->createContextMock());
-    }
-
-    public function testShouldDoNothingOnBeforeReceive()
-    {
-        $producer = $this->createSpoolProducerMock();
-        $producer
-            ->expects(self::never())
-            ->method('flush')
-        ;
-
-        $extension = new FlushSpoolProducerExtension($producer);
-        $extension->onBeforeReceive($this->createContextMock());
-    }
-
-    public function testShouldDoNothingOnPreReceived()
-    {
-        $producer = $this->createSpoolProducerMock();
-        $producer
-            ->expects(self::never())
-            ->method('flush')
-        ;
-
-        $extension = new FlushSpoolProducerExtension($producer);
-        $extension->onPreReceived($this->createContextMock());
-    }
-
-    public function testShouldDoNothingOnResult()
-    {
-        $producer = $this->createSpoolProducerMock();
-        $producer
-            ->expects(self::never())
-            ->method('flush')
-        ;
-
-        $extension = new FlushSpoolProducerExtension($producer);
-        $extension->onResult($this->createContextMock());
-    }
-
-    public function testShouldDoNothingOnIdle()
-    {
-        $producer = $this->createSpoolProducerMock();
-        $producer
-            ->expects(self::never())
-            ->method('flush')
-        ;
-
-        $extension = new FlushSpoolProducerExtension($producer);
-        $extension->onIdle($this->createContextMock());
-    }
-
-    public function testShouldFlushSpoolProducerOnInterrupted()
+    public function testShouldFlushSpoolProducerOnEnd()
     {
         $producer = $this->createSpoolProducerMock();
         $producer
@@ -91,8 +38,10 @@ public function testShouldFlushSpoolProducerOnInterrupted()
             ->method('flush')
         ;
 
+        $end = new End($this->createInteropContextMock(), 1, 2, new NullLogger());
+
         $extension = new FlushSpoolProducerExtension($producer);
-        $extension->onInterrupted($this->createContextMock());
+        $extension->onEnd($end);
     }
 
     public function testShouldFlushSpoolProducerOnPostReceived()
@@ -103,20 +52,29 @@ public function testShouldFlushSpoolProducerOnPostReceived()
             ->method('flush')
         ;
 
+        $context = new PostMessageReceived(
+            $this->createInteropContextMock(),
+            $this->createMock(Consumer::class),
+            $this->createMock(Message::class),
+            'aResult',
+            1,
+            new NullLogger()
+        );
+
         $extension = new FlushSpoolProducerExtension($producer);
-        $extension->onPostReceived($this->createContextMock());
+        $extension->onPostMessageReceived($context);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|Context
+     * @return MockObject
      */
-    private function createContextMock()
+    private function createInteropContextMock(): Context
     {
         return $this->createMock(Context::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|SpoolProducer
+     * @return MockObject|SpoolProducer
      */
     private function createSpoolProducerMock()
     {
diff --git a/Tests/Client/ConsumptionExtension/LogExtensionTest.php b/Tests/Client/ConsumptionExtension/LogExtensionTest.php
new file mode 100644
index 0000000..db75767
--- /dev/null
+++ b/Tests/Client/ConsumptionExtension/LogExtensionTest.php
@@ -0,0 +1,536 @@
+<?php
+
+namespace Enqueue\Tests\Client\ConsumptionExtension;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\ConsumptionExtension\LogExtension;
+use Enqueue\Consumption\Context\End;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\Start;
+use Enqueue\Consumption\EndExtensionInterface;
+use Enqueue\Consumption\MessageReceivedExtensionInterface;
+use Enqueue\Consumption\PostMessageReceivedExtensionInterface;
+use Enqueue\Consumption\Result;
+use Enqueue\Consumption\StartExtensionInterface;
+use Enqueue\Null\NullMessage;
+use Enqueue\Null\NullQueue;
+use Enqueue\Test\ClassExtensionTrait;
+use Enqueue\Util\Stringify;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Processor;
+use Interop\Queue\Queue;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
+
+class LogExtensionTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementStartExtensionInterface()
+    {
+        $this->assertClassImplements(StartExtensionInterface::class, LogExtension::class);
+    }
+
+    public function testShouldImplementEndExtensionInterface()
+    {
+        $this->assertClassImplements(EndExtensionInterface::class, LogExtension::class);
+    }
+
+    public function testShouldImplementMessageReceivedExtensionInterface()
+    {
+        $this->assertClassImplements(MessageReceivedExtensionInterface::class, LogExtension::class);
+    }
+
+    public function testShouldImplementPostMessageReceivedExtensionInterface()
+    {
+        $this->assertClassImplements(PostMessageReceivedExtensionInterface::class, LogExtension::class);
+    }
+
+    public function testShouldSubClassOfLogExtension()
+    {
+        $this->assertClassExtends(\Enqueue\Consumption\Extension\LogExtension::class, LogExtension::class);
+    }
+
+    public function testShouldLogStartOnStart()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('debug')
+            ->with('Consumption has started')
+        ;
+
+        $context = new Start($this->createContextMock(), $logger, [], 1, 1);
+
+        $extension = new LogExtension();
+        $extension->onStart($context);
+    }
+
+    public function testShouldLogEndOnEnd()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('debug')
+            ->with('Consumption has ended')
+        ;
+
+        $context = new End($this->createContextMock(), 1, 2, $logger);
+
+        $extension = new LogExtension();
+        $extension->onEnd($context);
+    }
+
+    public function testShouldLogMessageReceived()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('debug')
+            ->with('Received from {queueName}	{body}', [
+                'queueName' => 'aQueue',
+                'redelivered' => false,
+                'body' => Stringify::that('aBody'),
+                'properties' => Stringify::that(['aProp' => 'aPropVal']),
+                'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+            ])
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new MessageReceived($this->createContextMock(), $consumerMock, $message, $this->createProcessorMock(), 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onMessageReceived($context);
+    }
+
+    public function testShouldLogMessageProcessedWithStringResult()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::INFO,
+                'Processed from {queueName}	{body}	{result}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'aResult',
+                    'reason' => '',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, 'aResult', 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogRejectedMessageAsError()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::ERROR,
+                'Processed from {queueName}	{body}	{result}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'reject',
+                    'reason' => '',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Processor::REJECT, 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogMessageProcessedWithResultObject()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::INFO,
+                'Processed from {queueName}	{body}	{result}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'ack',
+                    'reason' => '',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack(), 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogMessageProcessedWithReasonResultObject()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::INFO,
+                'Processed from {queueName}	{body}	{result} {reason}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'ack',
+                    'reason' => 'aReason',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack('aReason'), 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogProcessedCommandMessageWithStringResult()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::INFO,
+                '[client] Processed {command}	{body}	{result}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal', Config::COMMAND => 'aCommand']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'aResult',
+                    'reason' => '',
+                    'command' => 'aCommand',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty(Config::COMMAND, 'aCommand');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, 'aResult', 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogRejectedCommandMessageAsError()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::ERROR,
+                '[client] Processed {command}	{body}	{result}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal', Config::COMMAND => 'aCommand']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'reject',
+                    'reason' => '',
+                    'command' => 'aCommand',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setProperty(Config::COMMAND, 'aCommand');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Processor::REJECT, 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogProcessedCommandMessageWithResultObject()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::INFO,
+                '[client] Processed {command}	{body}	{result}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal', Config::COMMAND => 'aCommand']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'ack',
+                    'reason' => '',
+                    'command' => 'aCommand',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setProperty(Config::COMMAND, 'aCommand');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack(), 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogProcessedCommandMessageWithReasonResultObject()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::INFO,
+                '[client] Processed {command}	{body}	{result} {reason}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal', Config::COMMAND => 'aCommand']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'ack',
+                    'reason' => 'aReason',
+                    'command' => 'aCommand',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setProperty(Config::COMMAND, 'aCommand');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack('aReason'), 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogProcessedTopicProcessorMessageWithStringResult()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::INFO,
+                '[client] Processed {topic} -> {processor}	{body}	{result}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal', Config::TOPIC => 'aTopic', Config::PROCESSOR => 'aProcessor']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'aResult',
+                    'reason' => '',
+                    'topic' => 'aTopic',
+                    'processor' => 'aProcessor',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty(Config::TOPIC, 'aTopic');
+        $message->setProperty(Config::PROCESSOR, 'aProcessor');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, 'aResult', 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogRejectedTopicProcessorMessageAsError()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::ERROR,
+                '[client] Processed {topic} -> {processor}	{body}	{result}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal', Config::TOPIC => 'aTopic', Config::PROCESSOR => 'aProcessor']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'reject',
+                    'reason' => '',
+                    'topic' => 'aTopic',
+                    'processor' => 'aProcessor',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty(Config::TOPIC, 'aTopic');
+        $message->setProperty(Config::PROCESSOR, 'aProcessor');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Processor::REJECT, 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogProcessedTopicProcessorMessageWithResultObject()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::INFO,
+                '[client] Processed {topic} -> {processor}	{body}	{result}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal', Config::TOPIC => 'aTopic', Config::PROCESSOR => 'aProcessor']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'ack',
+                    'reason' => '',
+                    'topic' => 'aTopic',
+                    'processor' => 'aProcessor',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty(Config::TOPIC, 'aTopic');
+        $message->setProperty(Config::PROCESSOR, 'aProcessor');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack(), 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogProcessedTopicProcessorMessageWithReasonResultObject()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::INFO,
+                '[client] Processed {topic} -> {processor}	{body}	{result} {reason}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal', Config::TOPIC => 'aTopic', Config::PROCESSOR => 'aProcessor']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'ack',
+                    'reason' => 'aReason',
+                    'topic' => 'aTopic',
+                    'processor' => 'aProcessor',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty(Config::TOPIC, 'aTopic');
+        $message->setProperty(Config::PROCESSOR, 'aProcessor');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack('aReason'), 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    /**
+     * @return MockObject
+     */
+    private function createConsumerStub(Queue $queue): Consumer
+    {
+        $consumerMock = $this->createMock(Consumer::class);
+        $consumerMock
+            ->expects($this->any())
+            ->method('getQueue')
+            ->willReturn($queue)
+        ;
+
+        return $consumerMock;
+    }
+
+    /**
+     * @return MockObject
+     */
+    private function createContextMock(): Context
+    {
+        return $this->createMock(Context::class);
+    }
+
+    /**
+     * @return MockObject
+     */
+    private function createProcessorMock(): Processor
+    {
+        return $this->createMock(Processor::class);
+    }
+
+    /**
+     * @return MockObject|LoggerInterface
+     */
+    private function createLogger()
+    {
+        return $this->createMock(LoggerInterface::class);
+    }
+}
diff --git a/Tests/Client/ConsumptionExtension/SetRouterPropertiesExtensionTest.php b/Tests/Client/ConsumptionExtension/SetRouterPropertiesExtensionTest.php
index d344ed6..d521aef 100644
--- a/Tests/Client/ConsumptionExtension/SetRouterPropertiesExtensionTest.php
+++ b/Tests/Client/ConsumptionExtension/SetRouterPropertiesExtensionTest.php
@@ -5,31 +5,32 @@
 use Enqueue\Client\Config;
 use Enqueue\Client\ConsumptionExtension\SetRouterPropertiesExtension;
 use Enqueue\Client\DriverInterface;
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\MessageReceivedExtensionInterface;
 use Enqueue\Null\NullMessage;
 use Enqueue\Null\NullQueue;
 use Enqueue\Test\ClassExtensionTrait;
-use Interop\Queue\PsrContext;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context as InteropContext;
+use Interop\Queue\Processor;
+use Interop\Queue\Queue;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
+use Psr\Log\NullLogger;
 
 class SetRouterPropertiesExtensionTest extends TestCase
 {
     use ClassExtensionTrait;
 
-    public function testShouldImplementExtensionInterface()
+    public function testShouldImplementMessageReceivedExtensionInterface()
     {
-        $this->assertClassImplements(ExtensionInterface::class, SetRouterPropertiesExtension::class);
-    }
-
-    public function testCouldBeConstructedWithRequiredArguments()
-    {
-        new SetRouterPropertiesExtension($this->createDriverMock());
+        $this->assertClassImplements(MessageReceivedExtensionInterface::class, SetRouterPropertiesExtension::class);
     }
 
     public function testShouldSetRouterProcessorPropertyIfNotSetAndOnRouterQueue()
     {
-        $config = Config::create('test', '', '', 'router-queue', '', 'router-processor-name');
+        $config = Config::create('test', '.', '', '', 'router-queue', '', 'router-processor-name');
+        $queue = new NullQueue('test.router-queue');
 
         $driver = $this->createDriverMock();
         $driver
@@ -38,24 +39,37 @@ public function testShouldSetRouterProcessorPropertyIfNotSetAndOnRouterQueue()
             ->willReturn($config)
         ;
 
+        $driver
+            ->expects($this->once())
+            ->method('createQueue')
+            ->willReturn($queue)
+        ;
+
         $message = new NullMessage();
+        $message->setProperty(Config::TOPIC, 'aTopic');
 
-        $context = new Context($this->createPsrContextMock());
-        $context->setPsrMessage($message);
-        $context->setPsrQueue(new NullQueue('test.router-queue'));
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub(new NullQueue('test.router-queue')),
+            $message,
+            $this->createProcessorMock(),
+            1,
+            new NullLogger()
+        );
 
         $extension = new SetRouterPropertiesExtension($driver);
-        $extension->onPreReceived($context);
+        $extension->onMessageReceived($messageReceived);
 
         $this->assertEquals([
-            'enqueue.processor_name' => 'router-processor-name',
-            'enqueue.processor_queue_name' => 'router-queue',
+            Config::PROCESSOR => 'router-processor-name',
+            Config::TOPIC => 'aTopic',
         ], $message->getProperties());
     }
 
     public function testShouldNotSetRouterProcessorPropertyIfNotSetAndNotOnRouterQueue()
     {
         $config = Config::create('test', '', '', 'router-queue', '', 'router-processor-name');
+        $queue = new NullQueue('test.router-queue');
 
         $driver = $this->createDriverMock();
         $driver
@@ -64,16 +78,30 @@ public function testShouldNotSetRouterProcessorPropertyIfNotSetAndNotOnRouterQue
             ->willReturn($config)
         ;
 
+        $driver
+            ->expects($this->once())
+            ->method('createQueue')
+            ->willReturn($queue)
+        ;
+
         $message = new NullMessage();
+        $message->setProperty(Config::TOPIC, 'aTopic');
 
-        $context = new Context($this->createPsrContextMock());
-        $context->setPsrMessage($message);
-        $context->setPsrQueue(new NullQueue('test.another-queue'));
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub(new NullQueue('test.another-queue')),
+            $message,
+            $this->createProcessorMock(),
+            1,
+            new NullLogger()
+        );
 
         $extension = new SetRouterPropertiesExtension($driver);
-        $extension->onPreReceived($context);
+        $extension->onMessageReceived($messageReceived);
 
-        $this->assertEquals([], $message->getProperties());
+        $this->assertEquals([
+            Config::TOPIC => 'aTopic',
+        ], $message->getProperties());
     }
 
     public function testShouldNotSetAnyPropertyIfProcessorNamePropertyAlreadySet()
@@ -85,32 +113,86 @@ public function testShouldNotSetAnyPropertyIfProcessorNamePropertyAlreadySet()
         ;
 
         $message = new NullMessage();
-        $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'non-router-processor');
+        $message->setProperty(Config::PROCESSOR, 'non-router-processor');
 
-        $context = new Context($this->createPsrContextMock());
-        $context->setPsrMessage($message);
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub(null),
+            $message,
+            $this->createProcessorMock(),
+            1,
+            new NullLogger()
+        );
 
         $extension = new SetRouterPropertiesExtension($driver);
-        $extension->onPreReceived($context);
+        $extension->onMessageReceived($messageReceived);
 
         $this->assertEquals([
-            'enqueue.processor_name' => 'non-router-processor',
+            'enqueue.processor' => 'non-router-processor',
         ], $message->getProperties());
     }
 
+    public function testShouldSkipMessagesWithoutTopicPropertySet()
+    {
+        $driver = $this->createDriverMock();
+        $driver
+            ->expects($this->never())
+            ->method('getConfig')
+        ;
+
+        $message = new NullMessage();
+
+        $messageReceived = new MessageReceived(
+            $this->createContextMock(),
+            $this->createConsumerStub(null),
+            $message,
+            $this->createProcessorMock(),
+            1,
+            new NullLogger()
+        );
+
+        $extension = new SetRouterPropertiesExtension($driver);
+        $extension->onMessageReceived($messageReceived);
+
+        $this->assertEquals([], $message->getProperties());
+    }
+
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext
+     * @return MockObject|InteropContext
      */
-    protected function createPsrContextMock()
+    protected function createContextMock(): InteropContext
     {
-        return $this->createMock(PsrContext::class);
+        return $this->createMock(InteropContext::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface
+     * @return MockObject|DriverInterface
      */
-    protected function createDriverMock()
+    protected function createDriverMock(): DriverInterface
     {
         return $this->createMock(DriverInterface::class);
     }
+
+    /**
+     * @return MockObject
+     */
+    private function createProcessorMock(): Processor
+    {
+        return $this->createMock(Processor::class);
+    }
+
+    /**
+     * @return MockObject|Consumer
+     */
+    private function createConsumerStub(?Queue $queue): Consumer
+    {
+        $consumerMock = $this->createMock(Consumer::class);
+        $consumerMock
+            ->expects($this->any())
+            ->method('getQueue')
+            ->willReturn($queue ?? new NullQueue('queue'))
+        ;
+
+        return $consumerMock;
+    }
 }
diff --git a/Tests/Client/ConsumptionExtension/SetupBrokerExtensionTest.php b/Tests/Client/ConsumptionExtension/SetupBrokerExtensionTest.php
index 21b011f..fbd3679 100644
--- a/Tests/Client/ConsumptionExtension/SetupBrokerExtensionTest.php
+++ b/Tests/Client/ConsumptionExtension/SetupBrokerExtensionTest.php
@@ -4,10 +4,11 @@
 
 use Enqueue\Client\ConsumptionExtension\SetupBrokerExtension;
 use Enqueue\Client\DriverInterface;
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\Context\Start;
+use Enqueue\Consumption\StartExtensionInterface;
 use Enqueue\Test\ClassExtensionTrait;
-use Interop\Queue\PsrContext;
+use Interop\Queue\Context as InteropContext;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Psr\Log\NullLogger;
 
@@ -15,14 +16,9 @@ class SetupBrokerExtensionTest extends TestCase
 {
     use ClassExtensionTrait;
 
-    public function testShouldImplementExtensionInterface()
+    public function testShouldImplementStartExtensionInterface()
     {
-        $this->assertClassImplements(ExtensionInterface::class, SetupBrokerExtension::class);
-    }
-
-    public function testCouldBeConstructedWithRequiredArguments()
-    {
-        new SetupBrokerExtension($this->createDriverMock());
+        $this->assertClassImplements(StartExtensionInterface::class, SetupBrokerExtension::class);
     }
 
     public function testShouldSetupBroker()
@@ -36,8 +32,7 @@ public function testShouldSetupBroker()
             ->with($this->identicalTo($logger))
         ;
 
-        $context = new Context($this->createMock(PsrContext::class));
-        $context->setLogger($logger);
+        $context = new Start($this->createMock(InteropContext::class), $logger, [], 0, 0);
 
         $extension = new SetupBrokerExtension($driver);
         $extension->onStart($context);
@@ -54,8 +49,7 @@ public function testShouldSetupBrokerOnlyOnce()
             ->with($this->identicalTo($logger))
         ;
 
-        $context = new Context($this->createMock(PsrContext::class));
-        $context->setLogger($logger);
+        $context = new Start($this->createMock(InteropContext::class), $logger, [], 0, 0);
 
         $extension = new SetupBrokerExtension($driver);
         $extension->onStart($context);
@@ -63,7 +57,7 @@ public function testShouldSetupBrokerOnlyOnce()
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface
+     * @return MockObject|DriverInterface
      */
     private function createDriverMock()
     {
diff --git a/Tests/Client/DelegateProcessorTest.php b/Tests/Client/DelegateProcessorTest.php
index f107dde..9743cf4 100644
--- a/Tests/Client/DelegateProcessorTest.php
+++ b/Tests/Client/DelegateProcessorTest.php
@@ -4,36 +4,30 @@
 
 use Enqueue\Client\Config;
 use Enqueue\Client\DelegateProcessor;
-use Enqueue\Client\ProcessorRegistryInterface;
 use Enqueue\Null\NullMessage;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrProcessor;
+use Enqueue\ProcessorRegistryInterface;
+use Interop\Queue\Context;
+use Interop\Queue\Processor;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 
 class DelegateProcessorTest extends TestCase
 {
-    public function testCouldBeConstructedWithRequiredArguments()
-    {
-        new DelegateProcessor($this->createProcessorRegistryMock());
-    }
-
     public function testShouldThrowExceptionIfProcessorNameIsNotSet()
     {
-        $this->setExpectedException(
-            \LogicException::class,
-            'Got message without required parameter: "enqueue.processor_name"'
-        );
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Got message without required parameter: "enqueue.processor"');
 
         $processor = new DelegateProcessor($this->createProcessorRegistryMock());
-        $processor->process(new NullMessage(), $this->createPsrContextMock());
+        $processor->process(new NullMessage(), $this->createContextMock());
     }
 
     public function testShouldProcessMessage()
     {
-        $session = $this->createPsrContextMock();
+        $session = $this->createContextMock();
         $message = new NullMessage();
         $message->setProperties([
-            Config::PARAMETER_PROCESSOR_NAME => 'processor-name',
+            Config::PROCESSOR => 'processor-name',
         ]);
 
         $processor = $this->createProcessorMock();
@@ -41,7 +35,7 @@ public function testShouldProcessMessage()
             ->expects($this->once())
             ->method('process')
             ->with($this->identicalTo($message), $this->identicalTo($session))
-            ->will($this->returnValue('return-value'))
+            ->willReturn('return-value')
         ;
 
         $processorRegistry = $this->createProcessorRegistryMock();
@@ -49,7 +43,7 @@ public function testShouldProcessMessage()
             ->expects($this->once())
             ->method('get')
             ->with('processor-name')
-            ->will($this->returnValue($processor))
+            ->willReturn($processor)
         ;
 
         $processor = new DelegateProcessor($processorRegistry);
@@ -59,7 +53,7 @@ public function testShouldProcessMessage()
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|ProcessorRegistryInterface
+     * @return MockObject|ProcessorRegistryInterface
      */
     protected function createProcessorRegistryMock()
     {
@@ -67,18 +61,18 @@ protected function createProcessorRegistryMock()
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext
+     * @return MockObject|Context
      */
-    protected function createPsrContextMock()
+    protected function createContextMock()
     {
-        return $this->createMock(PsrContext::class);
+        return $this->createMock(Context::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor
+     * @return MockObject|Processor
      */
     protected function createProcessorMock()
     {
-        return $this->createMock(PsrProcessor::class);
+        return $this->createMock(Processor::class);
     }
 }
diff --git a/Tests/Client/Driver/AmqpDriverTest.php b/Tests/Client/Driver/AmqpDriverTest.php
new file mode 100644
index 0000000..2cfb170
--- /dev/null
+++ b/Tests/Client/Driver/AmqpDriverTest.php
@@ -0,0 +1,360 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use DMS\PHPUnitExtensions\ArraySubset\Assert;
+use Enqueue\Client\Config;
+use Enqueue\Client\Driver\AmqpDriver;
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Message;
+use Enqueue\Client\MessagePriority;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Amqp\AmqpContext;
+use Interop\Amqp\AmqpProducer;
+use Interop\Amqp\Impl\AmqpBind;
+use Interop\Amqp\Impl\AmqpMessage;
+use Interop\Amqp\Impl\AmqpQueue;
+use Interop\Amqp\Impl\AmqpTopic;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use PHPUnit\Framework\TestCase;
+
+class AmqpDriverTest extends TestCase
+{
+    use ClassExtensionTrait;
+    use GenericDriverTestsTrait;
+
+    public function testShouldImplementsDriverInterface()
+    {
+        $this->assertClassImplements(DriverInterface::class, AmqpDriver::class);
+    }
+
+    public function testShouldBeSubClassOfGenericDriver()
+    {
+        $this->assertClassExtends(GenericDriver::class, AmqpDriver::class);
+    }
+
+    public function testThrowIfPriorityIsNotSupportedOnCreateTransportMessage()
+    {
+        $clientMessage = new Message();
+        $clientMessage->setPriority('invalidPriority');
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($this->createMessage())
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Cant convert client priority "invalidPriority" to transport one. Could be one of "enqueue.message_queue.client.very_low_message_priority", "enqueue.message_queue.client.low_message_priority", "enqueue.message_queue.client.normal_message_priority');
+        $driver->createTransportMessage($clientMessage);
+    }
+
+    public function testShouldSetExpirationHeaderFromClientMessageExpireInMillisecondsOnCreateTransportMessage()
+    {
+        $clientMessage = new Message();
+        $clientMessage->setExpire(333);
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($this->createMessage())
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        /** @var AmqpMessage $transportMessage */
+        $transportMessage = $driver->createTransportMessage($clientMessage);
+
+        $this->assertSame(333000, $transportMessage->getExpiration());
+        $this->assertSame('333000', $transportMessage->getHeader('expiration'));
+    }
+
+    public function testShouldSetPersistedDeliveryModeOnCreateTransportMessage()
+    {
+        $clientMessage = new Message();
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($this->createMessage())
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        /** @var AmqpMessage $transportMessage */
+        $transportMessage = $driver->createTransportMessage($clientMessage);
+
+        $this->assertSame(AmqpMessage::DELIVERY_MODE_PERSISTENT, $transportMessage->getDeliveryMode());
+    }
+
+    public function testShouldCreateDurableQueue()
+    {
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->willReturn($this->createQueue('aName'))
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        /** @var AmqpQueue $queue */
+        $queue = $driver->createQueue('aName');
+
+        $this->assertSame(AmqpQueue::FLAG_DURABLE, $queue->getFlags());
+    }
+
+    public function testShouldResetPriorityAndExpirationAndNeverCallProducerDeliveryDelayOnSendMessageToRouter()
+    {
+        $topic = $this->createTopic('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->with($this->identicalTo($topic), $this->identicalTo($transportMessage))
+        ;
+        $producer
+            ->expects($this->never())
+            ->method('setDeliveryDelay')
+        ;
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createTopic')
+            ->willReturn($topic)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::TOPIC, 'topic');
+        $message->setExpire(123);
+        $message->setPriority(MessagePriority::HIGH);
+
+        $driver->sendToRouter($message);
+
+        $this->assertNull($transportMessage->getExpiration());
+        $this->assertNull($transportMessage->getPriority());
+    }
+
+    public function testShouldSetupBroker()
+    {
+        $routerTopic = $this->createTopic('');
+        $routerQueue = $this->createQueue('');
+        $processorWithDefaultQueue = $this->createQueue('default');
+        $processorWithCustomQueue = $this->createQueue('custom');
+        $context = $this->createContextMock();
+        // setup router
+        $context
+            ->expects($this->at(0))
+            ->method('createTopic')
+            ->willReturn($routerTopic)
+        ;
+        $context
+            ->expects($this->at(1))
+            ->method('declareTopic')
+            ->with($this->identicalTo($routerTopic))
+        ;
+
+        $context
+            ->expects($this->at(2))
+            ->method('createQueue')
+            ->willReturn($routerQueue)
+        ;
+        $context
+            ->expects($this->at(3))
+            ->method('declareQueue')
+            ->with($this->identicalTo($routerQueue))
+        ;
+
+        $context
+            ->expects($this->at(4))
+            ->method('bind')
+            ->with($this->isInstanceOf(AmqpBind::class))
+        ;
+
+        // setup processor with default queue
+        $context
+            ->expects($this->at(5))
+            ->method('createQueue')
+            ->with($this->getDefaultQueueTransportName())
+            ->willReturn($processorWithDefaultQueue)
+        ;
+        $context
+            ->expects($this->at(6))
+            ->method('declareQueue')
+            ->with($this->identicalTo($processorWithDefaultQueue))
+        ;
+
+        $context
+            ->expects($this->at(7))
+            ->method('createQueue')
+            ->with($this->getCustomQueueTransportName())
+            ->willReturn($processorWithCustomQueue)
+        ;
+        $context
+            ->expects($this->at(8))
+            ->method('declareQueue')
+            ->with($this->identicalTo($processorWithCustomQueue))
+        ;
+
+        $driver = new AmqpDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('aTopic', Route::TOPIC, 'aProcessor'),
+                new Route('aCommand', Route::COMMAND, 'aProcessor', ['queue' => 'custom']),
+            ])
+        );
+        $driver->setupBroker();
+    }
+
+    public function testShouldNotDeclareSameQueues()
+    {
+        $context = $this->createContextMock();
+
+        // setup processor with default queue
+        $context
+            ->expects($this->any())
+            ->method('createTopic')
+            ->willReturn($this->createTopic(''))
+        ;
+        $context
+            ->expects($this->any())
+            ->method('createQueue')
+            ->willReturn($this->createQueue('custom'))
+        ;
+        $context
+            ->expects($this->exactly(2))
+            ->method('declareQueue')
+        ;
+
+        $driver = new AmqpDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('aTopic', Route::TOPIC, 'aProcessor', ['queue' => 'custom']),
+                new Route('aCommand', Route::COMMAND, 'aProcessor', ['queue' => 'custom']),
+            ])
+        );
+        $driver->setupBroker();
+    }
+
+    protected function createDriver(...$args): DriverInterface
+    {
+        return new AmqpDriver(...$args);
+    }
+
+    /**
+     * @return AmqpContext
+     */
+    protected function createContextMock(): Context
+    {
+        return $this->createMock(AmqpContext::class);
+    }
+
+    /**
+     * @return AmqpProducer
+     */
+    protected function createProducerMock(): InteropProducer
+    {
+        return $this->createMock(AmqpProducer::class);
+    }
+
+    /**
+     * @return AmqpQueue
+     */
+    protected function createQueue(string $name): InteropQueue
+    {
+        return new AmqpQueue($name);
+    }
+
+    protected function createTopic(string $name): AmqpTopic
+    {
+        return new AmqpTopic($name);
+    }
+
+    /**
+     * @return AmqpMessage
+     */
+    protected function createMessage(): InteropMessage
+    {
+        return new AmqpMessage();
+    }
+
+    protected function getRouterTransportName(): string
+    {
+        return 'aprefix.router';
+    }
+
+    protected function assertTransportMessage(InteropMessage $transportMessage): void
+    {
+        $this->assertSame('body', $transportMessage->getBody());
+        Assert::assertArraySubset([
+            'hkey' => 'hval',
+            'delivery_mode' => AmqpMessage::DELIVERY_MODE_PERSISTENT,
+            'content_type' => 'ContentType',
+            'expiration' => '123000',
+            'priority' => 3,
+            'message_id' => 'theMessageId',
+            'timestamp' => 1000,
+            'reply_to' => 'theReplyTo',
+            'correlation_id' => 'theCorrelationId',
+        ], $transportMessage->getHeaders());
+        $this->assertEquals([
+            'pkey' => 'pval',
+            Config::CONTENT_TYPE => 'ContentType',
+            Config::PRIORITY => MessagePriority::HIGH,
+            Config::EXPIRE => 123,
+            Config::DELAY => 345,
+        ], $transportMessage->getProperties());
+        $this->assertSame('theMessageId', $transportMessage->getMessageId());
+        $this->assertSame(1000, $transportMessage->getTimestamp());
+        $this->assertSame('theReplyTo', $transportMessage->getReplyTo());
+        $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId());
+    }
+}
diff --git a/Tests/Client/Driver/DbalDriverTest.php b/Tests/Client/Driver/DbalDriverTest.php
new file mode 100644
index 0000000..554a399
--- /dev/null
+++ b/Tests/Client/Driver/DbalDriverTest.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use Enqueue\Client\Driver\DbalDriver;
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Dbal\DbalContext;
+use Enqueue\Dbal\DbalDestination;
+use Enqueue\Dbal\DbalMessage;
+use Enqueue\Dbal\DbalProducer;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use Interop\Queue\Topic as InteropTopic;
+use PHPUnit\Framework\TestCase;
+
+class DbalDriverTest extends TestCase
+{
+    use ClassExtensionTrait;
+    use GenericDriverTestsTrait;
+
+    public function testShouldImplementsDriverInterface()
+    {
+        $this->assertClassImplements(DriverInterface::class, DbalDriver::class);
+    }
+
+    public function testShouldBeSubClassOfGenericDriver()
+    {
+        $this->assertClassExtends(GenericDriver::class, DbalDriver::class);
+    }
+
+    public function testShouldSetupBroker()
+    {
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('getTableName')
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createDataBaseTable')
+        ;
+
+        $driver = new DbalDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $driver->setupBroker();
+    }
+
+    protected function createDriver(...$args): DriverInterface
+    {
+        return new DbalDriver(...$args);
+    }
+
+    /**
+     * @return DbalContext
+     */
+    protected function createContextMock(): Context
+    {
+        return $this->createMock(DbalContext::class);
+    }
+
+    /**
+     * @return DbalProducer
+     */
+    protected function createProducerMock(): InteropProducer
+    {
+        return $this->createMock(DbalProducer::class);
+    }
+
+    /**
+     * @return DbalDestination
+     */
+    protected function createQueue(string $name): InteropQueue
+    {
+        return new DbalDestination($name);
+    }
+
+    /**
+     * @return DbalDestination
+     */
+    protected function createTopic(string $name): InteropTopic
+    {
+        return new DbalDestination($name);
+    }
+
+    /**
+     * @return DbalMessage
+     */
+    protected function createMessage(): InteropMessage
+    {
+        return new DbalMessage();
+    }
+}
diff --git a/Tests/Client/Driver/FsDriverTest.php b/Tests/Client/Driver/FsDriverTest.php
new file mode 100644
index 0000000..f1cd02f
--- /dev/null
+++ b/Tests/Client/Driver/FsDriverTest.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use Enqueue\Client\Driver\FsDriver;
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Fs\FsContext;
+use Enqueue\Fs\FsDestination;
+use Enqueue\Fs\FsMessage;
+use Enqueue\Fs\FsProducer;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use Interop\Queue\Topic as InteropTopic;
+use Makasim\File\TempFile;
+use PHPUnit\Framework\TestCase;
+
+class FsDriverTest extends TestCase
+{
+    use ClassExtensionTrait;
+    use GenericDriverTestsTrait;
+
+    public function testShouldImplementsDriverInterface()
+    {
+        $this->assertClassImplements(DriverInterface::class, FsDriver::class);
+    }
+
+    public function testShouldBeSubClassOfGenericDriver()
+    {
+        $this->assertClassExtends(GenericDriver::class, FsDriver::class);
+    }
+
+    public function testShouldSetupBroker()
+    {
+        $routerQueue = new FsDestination(TempFile::generate());
+
+        $processorQueue = new FsDestination(TempFile::generate());
+
+        $context = $this->createContextMock();
+        // setup router
+        $context
+            ->expects($this->at(0))
+            ->method('createQueue')
+            ->willReturn($routerQueue)
+        ;
+        $context
+            ->expects($this->at(1))
+            ->method('declareDestination')
+            ->with($this->identicalTo($routerQueue))
+        ;
+        // setup processor queue
+        $context
+            ->expects($this->at(2))
+            ->method('createQueue')
+            ->willReturn($processorQueue)
+        ;
+        $context
+            ->expects($this->at(3))
+            ->method('declareDestination')
+            ->with($this->identicalTo($processorQueue))
+        ;
+
+        $routeCollection = new RouteCollection([
+            new Route('aTopic', Route::TOPIC, 'aProcessor'),
+        ]);
+
+        $driver = new FsDriver(
+            $context,
+            $this->createDummyConfig(),
+            $routeCollection
+        );
+
+        $driver->setupBroker();
+    }
+
+    protected function createDriver(...$args): DriverInterface
+    {
+        return new FsDriver(...$args);
+    }
+
+    /**
+     * @return FsContext
+     */
+    protected function createContextMock(): Context
+    {
+        return $this->createMock(FsContext::class);
+    }
+
+    /**
+     * @return FsProducer
+     */
+    protected function createProducerMock(): InteropProducer
+    {
+        return $this->createMock(FsProducer::class);
+    }
+
+    /**
+     * @return FsDestination
+     */
+    protected function createQueue(string $name): InteropQueue
+    {
+        return new FsDestination(new \SplFileInfo($name));
+    }
+
+    /**
+     * @return FsDestination
+     */
+    protected function createTopic(string $name): InteropTopic
+    {
+        return new FsDestination(new \SplFileInfo($name));
+    }
+
+    /**
+     * @return FsMessage
+     */
+    protected function createMessage(): InteropMessage
+    {
+        return new FsMessage();
+    }
+}
diff --git a/Tests/Client/Driver/GenericDriverTest.php b/Tests/Client/Driver/GenericDriverTest.php
new file mode 100644
index 0000000..78f7f6e
--- /dev/null
+++ b/Tests/Client/Driver/GenericDriverTest.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use DMS\PHPUnitExtensions\ArraySubset\Assert;
+use Enqueue\Client\Config;
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\MessagePriority;
+use Enqueue\Null\NullMessage;
+use Enqueue\Null\NullQueue;
+use Enqueue\Null\NullTopic;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use Interop\Queue\Topic as InteropTopic;
+use PHPUnit\Framework\TestCase;
+
+class GenericDriverTest extends TestCase
+{
+    use ClassExtensionTrait;
+    use GenericDriverTestsTrait;
+
+    public function testShouldImplementsDriverInterface()
+    {
+        $this->assertClassImplements(DriverInterface::class, GenericDriver::class);
+    }
+
+    protected function createDriver(...$args): DriverInterface
+    {
+        return new GenericDriver(...$args);
+    }
+
+    protected function createContextMock(): Context
+    {
+        return $this->createMock(Context::class);
+    }
+
+    protected function createProducerMock(): InteropProducer
+    {
+        return $this->createMock(InteropProducer::class);
+    }
+
+    protected function createQueue(string $name): InteropQueue
+    {
+        return new NullQueue($name);
+    }
+
+    protected function createTopic(string $name): InteropTopic
+    {
+        return new NullTopic($name);
+    }
+
+    protected function createMessage(): InteropMessage
+    {
+        return new NullMessage();
+    }
+
+    protected function assertTransportMessage(InteropMessage $transportMessage): void
+    {
+        $this->assertSame('body', $transportMessage->getBody());
+        Assert::assertArraySubset([
+            'hkey' => 'hval',
+            'message_id' => 'theMessageId',
+            'timestamp' => 1000,
+            'reply_to' => 'theReplyTo',
+            'correlation_id' => 'theCorrelationId',
+        ], $transportMessage->getHeaders());
+        $this->assertEquals([
+            'pkey' => 'pval',
+            Config::CONTENT_TYPE => 'ContentType',
+            Config::PRIORITY => MessagePriority::HIGH,
+            Config::EXPIRE => 123,
+            Config::DELAY => 345,
+        ], $transportMessage->getProperties());
+        $this->assertSame('theMessageId', $transportMessage->getMessageId());
+        $this->assertSame(1000, $transportMessage->getTimestamp());
+        $this->assertSame('theReplyTo', $transportMessage->getReplyTo());
+        $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId());
+    }
+}
diff --git a/Tests/Client/Driver/GenericDriverTestsTrait.php b/Tests/Client/Driver/GenericDriverTestsTrait.php
new file mode 100644
index 0000000..d5ad498
--- /dev/null
+++ b/Tests/Client/Driver/GenericDriverTestsTrait.php
@@ -0,0 +1,1249 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use DMS\PHPUnitExtensions\ArraySubset\Assert;
+use Enqueue\Client\Config;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Message;
+use Enqueue\Client\MessagePriority;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Interop\Queue\Context;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use Interop\Queue\Topic as InteropTopic;
+
+trait GenericDriverTestsTrait
+{
+    public function testCouldBeConstructedWithRequiredArguments()
+    {
+        $driver = $this->createDriver(
+            $this->createContextMock(),
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $this->assertInstanceOf(DriverInterface::class, $driver);
+    }
+
+    public function testShouldReturnContextSetInConstructor()
+    {
+        $context = $this->createContextMock();
+
+        $driver = $this->createDriver($context, $this->createDummyConfig(), new RouteCollection([]));
+
+        $this->assertSame($context, $driver->getContext());
+    }
+
+    public function testShouldReturnConfigObjectSetInConstructor()
+    {
+        $config = $this->createDummyConfig();
+
+        $driver = $this->createDriver($this->createContextMock(), $config, new RouteCollection([]));
+
+        $this->assertSame($config, $driver->getConfig());
+    }
+
+    public function testShouldReturnRouteCollectionSetInConstructor()
+    {
+        $routeCollection = new RouteCollection([]);
+
+        /** @var DriverInterface $driver */
+        $driver = $this->createDriver($this->createContextMock(), $this->createDummyConfig(), $routeCollection);
+
+        $this->assertSame($routeCollection, $driver->getRouteCollection());
+    }
+
+    public function testShouldCreateAndReturnQueueInstanceWithPrefixAndAppName()
+    {
+        $expectedQueue = $this->createQueue('aName');
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getPrefixAppFooQueueTransportName())
+            ->willReturn($expectedQueue)
+        ;
+
+        $config = new Config(
+            'aPrefix',
+            '.',
+            'anAppName',
+            'aRouterTopicName',
+            'aRouterQueueName',
+            'aDefaultQueue',
+            'aRouterProcessor',
+            [],
+            []
+        );
+
+        $driver = $this->createDriver($context, $config, new RouteCollection([]));
+
+        $queue = $driver->createQueue('aFooQueue');
+
+        $this->assertSame($expectedQueue, $queue);
+    }
+
+    public function testShouldCreateAndReturnQueueInstanceWithPrefixWithoutAppName()
+    {
+        $expectedQueue = $this->createQueue('aName');
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getPrefixFooQueueTransportName())
+            ->willReturn($expectedQueue)
+        ;
+
+        $config = new Config(
+            'aPrefix',
+            '.',
+            '',
+            'aRouterTopicName',
+            'aRouterQueueName',
+            'aDefaultQueue',
+            'aRouterProcessor',
+            [],
+            []
+        );
+
+        $driver = $this->createDriver($context, $config, new RouteCollection([]));
+
+        $queue = $driver->createQueue('aFooQueue');
+
+        $this->assertSame($expectedQueue, $queue);
+    }
+
+    public function testShouldCreateAndReturnQueueInstanceWithAppNameAndWithoutPrefix()
+    {
+        $expectedQueue = $this->createQueue('aName');
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getAppFooQueueTransportName())
+            ->willReturn($expectedQueue)
+        ;
+
+        $config = new Config(
+            '',
+            '.',
+            'anAppName',
+            'aRouterTopicName',
+            'aRouterQueueName',
+            'aDefaultQueue',
+            'aRouterProcessor',
+            [],
+            []
+        );
+
+        $driver = $this->createDriver($context, $config, new RouteCollection([]));
+
+        $queue = $driver->createQueue('aFooQueue');
+
+        $this->assertSame($expectedQueue, $queue);
+    }
+
+    public function testShouldCreateAndReturnQueueInstanceWithoutPrefixAndAppName()
+    {
+        $expectedQueue = $this->createQueue('aName');
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with('afooqueue')
+            ->willReturn($expectedQueue)
+        ;
+
+        $config = new Config(
+            '',
+            '.',
+            '',
+            'aRouterTopicName',
+            'aRouterQueueName',
+            'aDefaultQueue',
+            'aRouterProcessor',
+            [],
+            []
+        );
+
+        $driver = $this->createDriver($context, $config, new RouteCollection([]));
+
+        $queue = $driver->createQueue('aFooQueue');
+
+        $this->assertSame($expectedQueue, $queue);
+    }
+
+    public function testShouldCreateAndReturnQueueInstance()
+    {
+        $expectedQueue = $this->createQueue('aName');
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getPrefixFooQueueTransportName())
+            ->willReturn($expectedQueue)
+        ;
+
+        $driver = $this->createDriver($context, $this->createDummyConfig(), new RouteCollection([]));
+
+        $queue = $driver->createQueue('aFooQueue');
+
+        $this->assertSame($expectedQueue, $queue);
+    }
+
+    public function testShouldCreateClientMessageFromTransportOne()
+    {
+        $transportMessage = $this->createMessage();
+        $transportMessage->setBody('body');
+        $transportMessage->setHeaders(['hkey' => 'hval']);
+        $transportMessage->setProperty('pkey', 'pval');
+        $transportMessage->setProperty(Config::CONTENT_TYPE, 'theContentType');
+        $transportMessage->setProperty(Config::EXPIRE, '22');
+        $transportMessage->setProperty(Config::PRIORITY, MessagePriority::HIGH);
+        $transportMessage->setProperty('enqueue.delay', '44');
+        $transportMessage->setMessageId('theMessageId');
+        $transportMessage->setTimestamp(1000);
+        $transportMessage->setReplyTo('theReplyTo');
+        $transportMessage->setCorrelationId('theCorrelationId');
+
+        $driver = $this->createDriver(
+            $this->createContextMock(),
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $clientMessage = $driver->createClientMessage($transportMessage);
+
+        $this->assertClientMessage($clientMessage);
+    }
+
+    public function testShouldCreateTransportMessageFromClientOne()
+    {
+        $clientMessage = new Message();
+        $clientMessage->setBody('body');
+        $clientMessage->setHeaders(['hkey' => 'hval']);
+        $clientMessage->setProperties(['pkey' => 'pval']);
+        $clientMessage->setContentType('ContentType');
+        $clientMessage->setExpire(123);
+        $clientMessage->setDelay(345);
+        $clientMessage->setPriority(MessagePriority::HIGH);
+        $clientMessage->setMessageId('theMessageId');
+        $clientMessage->setTimestamp(1000);
+        $clientMessage->setReplyTo('theReplyTo');
+        $clientMessage->setCorrelationId('theCorrelationId');
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($this->createMessage())
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $transportMessage = $driver->createTransportMessage($clientMessage);
+
+        $this->assertTransportMessage($transportMessage);
+    }
+
+    public function testShouldSendMessageToRouter()
+    {
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->willReturnCallback(function (Destination $topic, InteropMessage $message) use ($transportMessage) {
+                $this->assertSame(
+                    $this->getRouterTransportName(),
+                    $topic instanceof InteropTopic ? $topic->getTopicName() : $topic->getQueueName());
+                $this->assertSame($transportMessage, $message);
+            })
+        ;
+        $context = $this->createContextStub();
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::TOPIC, 'topic');
+
+        $driver->sendToRouter($message);
+    }
+
+    public function testShouldNotInitDeliveryDelayOnSendMessageToRouter()
+    {
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('send')
+        ;
+        $producer
+            ->expects($this->never())
+            ->method('setDeliveryDelay')
+        ;
+
+        $context = $this->createContextStub();
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $message = new Message();
+        $message->setDelay(456);
+        $message->setProperty(Config::TOPIC, 'topic');
+
+        $driver->sendToRouter($message);
+    }
+
+    public function testShouldNotInitTimeToLiveOnSendMessageToRouter()
+    {
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('send')
+        ;
+        $producer
+            ->expects($this->never())
+            ->method('setTimeToLive')
+        ;
+
+        $context = $this->createContextStub();
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $message = new Message();
+        $message->setExpire(456);
+        $message->setProperty(Config::TOPIC, 'topic');
+
+        $driver->sendToRouter($message);
+    }
+
+    public function testShouldNotInitPriorityOnSendMessageToRouter()
+    {
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('send')
+        ;
+        $producer
+            ->expects($this->never())
+            ->method('setPriority')
+        ;
+
+        $context = $this->createContextStub();
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $message = new Message();
+        $message->setPriority(MessagePriority::HIGH);
+        $message->setProperty(Config::TOPIC, 'topic');
+
+        $driver->sendToRouter($message);
+    }
+
+    public function testThrowIfTopicIsNotSetOnSendToRouter()
+    {
+        $driver = $this->createDriver(
+            $this->createContextMock(),
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Topic name parameter is required but is not set');
+
+        $driver->sendToRouter(new Message());
+    }
+
+    public function testThrowIfCommandSetOnSendToRouter()
+    {
+        $driver = $this->createDriver(
+            $this->createContextMock(),
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::COMMAND, 'aCommand');
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Command must not be send to router but go directly to its processor.');
+
+        $driver->sendToRouter($message);
+    }
+
+    public function testShouldSendMessageToRouterProcessor()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getDefaultQueueTransportName())
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $config = $this->createDummyConfig();
+
+        $driver = $this->createDriver(
+            $context,
+            $config,
+            new RouteCollection([
+                new Route('topic', Route::TOPIC, 'processor', [
+                    'queue' => 'custom',
+                ]),
+            ])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::TOPIC, 'topic');
+        $message->setProperty(Config::PROCESSOR, $config->getRouterProcessor());
+
+        $driver->sendToProcessor($message);
+    }
+
+    public function testShouldSendTopicMessageToProcessorToDefaultQueue()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getDefaultQueueTransportName())
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('topic', Route::TOPIC, 'processor'),
+            ])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::TOPIC, 'topic');
+        $message->setProperty(Config::PROCESSOR, 'processor');
+
+        $driver->sendToProcessor($message);
+    }
+
+    public function testShouldSendTopicMessageToProcessorToCustomQueue()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getCustomQueueTransportName())
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('topic', Route::TOPIC, 'processor', ['queue' => 'custom']),
+            ])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::TOPIC, 'topic');
+        $message->setProperty(Config::PROCESSOR, 'processor');
+
+        $driver->sendToProcessor($message);
+    }
+
+    public function testShouldInitDeliveryDelayIfDelayPropertyOnSendToProcessor()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('setDeliveryDelay')
+            ->with(456000)
+        ;
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getDefaultQueueTransportName())
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('topic', Route::TOPIC, 'processor'),
+            ])
+        );
+
+        $message = new Message();
+        $message->setDelay(456);
+        $message->setProperty(Config::TOPIC, 'topic');
+        $message->setProperty(Config::PROCESSOR, 'processor');
+
+        $driver->sendToProcessor($message);
+    }
+
+    public function testShouldSetInitTimeToLiveIfExpirePropertyOnSendToProcessor()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('setTimeToLive')
+            ->with(678000)
+        ;
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getDefaultQueueTransportName())
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('topic', Route::TOPIC, 'processor'),
+            ])
+        );
+
+        $message = new Message();
+        $message->setExpire(678);
+        $message->setProperty(Config::TOPIC, 'topic');
+        $message->setProperty(Config::PROCESSOR, 'processor');
+
+        $driver->sendToProcessor($message);
+    }
+
+    public function testShouldSetInitPriorityIfPriorityPropertyOnSendToProcessor()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('setPriority')
+            ->with(3)
+        ;
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getDefaultQueueTransportName())
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('topic', Route::TOPIC, 'processor'),
+            ])
+        );
+
+        $message = new Message();
+        $message->setPriority(MessagePriority::HIGH);
+        $message->setProperty(Config::TOPIC, 'topic');
+        $message->setProperty(Config::PROCESSOR, 'processor');
+
+        $driver->sendToProcessor($message);
+    }
+
+    public function testThrowIfNoRouteFoundForTopicMessageOnSendToProcessor()
+    {
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->never())
+            ->method('createProducer')
+        ;
+        $context
+            ->expects($this->never())
+            ->method('createMessage')
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::TOPIC, 'topic');
+        $message->setProperty(Config::PROCESSOR, 'processor');
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('There is no route for topic "topic" and processor "processor"');
+        $driver->sendToProcessor($message);
+    }
+
+    public function testShouldSetRouterProcessorIfProcessorPropertyEmptyOnSendToProcessor()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getDefaultQueueTransportName())
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('topic', Route::TOPIC, 'expectedProcessor'),
+            ])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::TOPIC, 'topic');
+
+        $driver->sendToProcessor($message);
+
+        $this->assertSame('router', $message->getProperty(Config::PROCESSOR));
+    }
+
+    public function testShouldSendCommandMessageToProcessorToDefaultQueue()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getDefaultQueueTransportName())
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('command', Route::COMMAND, 'processor'),
+            ])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::COMMAND, 'command');
+        $message->setProperty(Config::PROCESSOR, 'processor');
+
+        $driver->sendToProcessor($message);
+    }
+
+    public function testShouldSendCommandMessageToProcessorToCustomQueue()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getCustomQueueTransportName())
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('command', Route::COMMAND, 'processor', ['queue' => 'custom']),
+            ])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::COMMAND, 'command');
+        $message->setProperty(Config::PROCESSOR, 'processor');
+
+        $driver->sendToProcessor($message);
+    }
+
+    public function testThrowIfNoRouteFoundForCommandMessageOnSendToProcessor()
+    {
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->never())
+            ->method('createProducer')
+        ;
+        $context
+            ->expects($this->never())
+            ->method('createMessage')
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::COMMAND, 'command');
+        $message->setProperty(Config::PROCESSOR, 'processor');
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('There is no route for command "command".');
+        $driver->sendToProcessor($message);
+    }
+
+    public function testShouldOverwriteProcessorPropertySetByOneFromCommandRouteOnSendToProcessor()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->with($this->identicalTo($queue), $this->identicalTo($transportMessage))
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with($this->getCustomQueueTransportName())
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('command', Route::COMMAND, 'expectedProcessor', ['queue' => 'custom']),
+            ])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::COMMAND, 'command');
+        $message->setProperty(Config::PROCESSOR, 'processorShouldBeOverwritten');
+
+        $driver->sendToProcessor($message);
+
+        $this->assertSame('expectedProcessor', $message->getProperty(Config::PROCESSOR));
+    }
+
+    public function testShouldNotInitDeliveryDelayOnSendMessageToProcessorIfPropertyNull()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->never())
+            ->method('setDeliveryDelay')
+        ;
+        $producer
+            ->expects($this->once())
+            ->method('send')
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('command', Route::COMMAND, 'expectedProcessor', ['queue' => 'custom']),
+            ])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::COMMAND, 'command');
+        $message->setDelay(null);
+
+        $driver->sendToProcessor($message);
+    }
+
+    public function testShouldNotInitPriorityOnSendMessageToProcessorIfPropertyNull()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->never())
+            ->method('setPriority')
+        ;
+        $producer
+            ->expects($this->once())
+            ->method('send')
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('command', Route::COMMAND, 'expectedProcessor', ['queue' => 'custom']),
+            ])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::COMMAND, 'command');
+        $message->setPriority(null);
+
+        $driver->sendToProcessor($message);
+    }
+
+    public function testShouldNotInitTimeToLiveOnSendMessageToProcessorIfPropertyNull()
+    {
+        $queue = $this->createQueue('');
+        $transportMessage = $this->createMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->never())
+            ->method('setTimeToLive')
+        ;
+        $producer
+            ->expects($this->once())
+            ->method('send')
+        ;
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('command', Route::COMMAND, 'expectedProcessor', ['queue' => 'custom']),
+            ])
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::COMMAND, 'command');
+        $message->setExpire(null);
+
+        $driver->sendToProcessor($message);
+    }
+
+    public function testThrowIfNeitherTopicNorCommandAreSentOnSendToProcessor()
+    {
+        $driver = $this->createDriver(
+            $this->createContextMock(),
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Queue name parameter is required but is not set');
+
+        $message = new Message();
+        $message->setProperty(Config::PROCESSOR, 'processor');
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Either topic or command parameter must be set.');
+        $driver->sendToProcessor($message);
+    }
+
+    abstract protected function createDriver(...$args): DriverInterface;
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    abstract protected function createContextMock(): Context;
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    abstract protected function createProducerMock(): InteropProducer;
+
+    abstract protected function createQueue(string $name): InteropQueue;
+
+    abstract protected function createTopic(string $name): InteropTopic;
+
+    abstract protected function createMessage(): InteropMessage;
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    protected function createContextStub(): Context
+    {
+        $context = $this->createContextMock();
+
+        $context
+            ->expects($this->any())
+            ->method('createQueue')
+            ->willReturnCallback(function (string $name) {
+                return $this->createQueue($name);
+            })
+        ;
+
+        $context
+            ->expects($this->any())
+            ->method('createTopic')
+            ->willReturnCallback(function (string $name) {
+                return $this->createTopic($name);
+            })
+        ;
+
+        return $context;
+    }
+
+    protected function assertTransportMessage(InteropMessage $transportMessage): void
+    {
+        $this->assertSame('body', $transportMessage->getBody());
+        $this->assertEquals([
+            'hkey' => 'hval',
+            'message_id' => 'theMessageId',
+            'timestamp' => 1000,
+            'reply_to' => 'theReplyTo',
+            'correlation_id' => 'theCorrelationId',
+        ], $transportMessage->getHeaders());
+        $this->assertEquals([
+            'pkey' => 'pval',
+            Config::CONTENT_TYPE => 'ContentType',
+            Config::PRIORITY => MessagePriority::HIGH,
+            Config::EXPIRE => 123,
+            Config::DELAY => 345,
+        ], $transportMessage->getProperties());
+        $this->assertSame('theMessageId', $transportMessage->getMessageId());
+        $this->assertSame(1000, $transportMessage->getTimestamp());
+        $this->assertSame('theReplyTo', $transportMessage->getReplyTo());
+        $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId());
+    }
+
+    protected function assertClientMessage(Message $clientMessage): void
+    {
+        $this->assertSame('body', $clientMessage->getBody());
+        Assert::assertArraySubset([
+            'hkey' => 'hval',
+        ], $clientMessage->getHeaders());
+        Assert::assertArraySubset([
+            'pkey' => 'pval',
+            Config::CONTENT_TYPE => 'theContentType',
+            Config::EXPIRE => '22',
+            Config::PRIORITY => MessagePriority::HIGH,
+            Config::DELAY => '44',
+        ], $clientMessage->getProperties());
+        $this->assertSame('theMessageId', $clientMessage->getMessageId());
+        $this->assertSame(22, $clientMessage->getExpire());
+        $this->assertSame(44, $clientMessage->getDelay());
+        $this->assertSame(MessagePriority::HIGH, $clientMessage->getPriority());
+        $this->assertSame('theContentType', $clientMessage->getContentType());
+        $this->assertSame(1000, $clientMessage->getTimestamp());
+        $this->assertSame('theReplyTo', $clientMessage->getReplyTo());
+        $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId());
+    }
+
+    protected function createDummyConfig(): Config
+    {
+        return Config::create('aPrefix');
+    }
+
+    protected function getDefaultQueueTransportName(): string
+    {
+        return 'aprefix.default';
+    }
+
+    protected function getCustomQueueTransportName(): string
+    {
+        return 'aprefix.custom';
+    }
+
+    protected function getRouterTransportName(): string
+    {
+        return 'aprefix.default';
+    }
+
+    protected function getPrefixAppFooQueueTransportName(): string
+    {
+        return 'aprefix.anappname.afooqueue';
+    }
+
+    protected function getPrefixFooQueueTransportName(): string
+    {
+        return 'aprefix.afooqueue';
+    }
+
+    protected function getAppFooQueueTransportName(): string
+    {
+        return 'anappname.afooqueue';
+    }
+}
diff --git a/Tests/Client/Driver/GpsDriverTest.php b/Tests/Client/Driver/GpsDriverTest.php
new file mode 100644
index 0000000..c0cac04
--- /dev/null
+++ b/Tests/Client/Driver/GpsDriverTest.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\Driver\GpsDriver;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Gps\GpsContext;
+use Enqueue\Gps\GpsMessage;
+use Enqueue\Gps\GpsProducer;
+use Enqueue\Gps\GpsQueue;
+use Enqueue\Gps\GpsTopic;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use Interop\Queue\Topic as InteropTopic;
+use PHPUnit\Framework\TestCase;
+
+class GpsDriverTest extends TestCase
+{
+    use ClassExtensionTrait;
+    use GenericDriverTestsTrait;
+
+    public function testShouldImplementsDriverInterface()
+    {
+        $this->assertClassImplements(DriverInterface::class, GpsDriver::class);
+    }
+
+    public function testShouldBeSubClassOfGenericDriver()
+    {
+        $this->assertClassExtends(GenericDriver::class, GpsDriver::class);
+    }
+
+    public function testShouldSetupBroker()
+    {
+        $routerTopic = new GpsTopic('');
+        $routerQueue = new GpsQueue('');
+
+        $processorTopic = new GpsTopic($this->getDefaultQueueTransportName());
+        $processorQueue = new GpsQueue($this->getDefaultQueueTransportName());
+
+        $context = $this->createContextMock();
+        // setup router
+        $context
+            ->expects($this->at(0))
+            ->method('createTopic')
+            ->willReturn($routerTopic)
+        ;
+        $context
+            ->expects($this->at(1))
+            ->method('createQueue')
+            ->willReturn($routerQueue)
+        ;
+        $context
+            ->expects($this->at(2))
+            ->method('subscribe')
+            ->with($this->identicalTo($routerTopic), $this->identicalTo($routerQueue))
+        ;
+        $context
+            ->expects($this->at(3))
+            ->method('createQueue')
+            ->with($this->getDefaultQueueTransportName())
+            ->willReturn($processorQueue)
+        ;
+        // setup processor queue
+        $context
+            ->expects($this->at(4))
+            ->method('createTopic')
+            ->with($this->getDefaultQueueTransportName())
+            ->willReturn($processorTopic)
+        ;
+        $context
+            ->expects($this->at(5))
+            ->method('subscribe')
+            ->with($this->identicalTo($processorTopic), $this->identicalTo($processorQueue))
+        ;
+
+        $driver = new GpsDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('aTopic', Route::TOPIC, 'aProcessor'),
+            ])
+        );
+
+        $driver->setupBroker();
+    }
+
+    protected function createDriver(...$args): DriverInterface
+    {
+        return new GpsDriver(...$args);
+    }
+
+    /**
+     * @return GpsContext
+     */
+    protected function createContextMock(): Context
+    {
+        return $this->createMock(GpsContext::class);
+    }
+
+    /**
+     * @return GpsProducer
+     */
+    protected function createProducerMock(): InteropProducer
+    {
+        return $this->createMock(GpsProducer::class);
+    }
+
+    /**
+     * @return GpsQueue
+     */
+    protected function createQueue(string $name): InteropQueue
+    {
+        return new GpsQueue($name);
+    }
+
+    /**
+     * @return GpsTopic
+     */
+    protected function createTopic(string $name): InteropTopic
+    {
+        return new GpsTopic($name);
+    }
+
+    /**
+     * @return GpsMessage
+     */
+    protected function createMessage(): InteropMessage
+    {
+        return new GpsMessage();
+    }
+
+    protected function getRouterTransportName(): string
+    {
+        return 'aprefix.router';
+    }
+}
diff --git a/Tests/Client/Driver/MongodbDriverTest.php b/Tests/Client/Driver/MongodbDriverTest.php
new file mode 100644
index 0000000..697c757
--- /dev/null
+++ b/Tests/Client/Driver/MongodbDriverTest.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\Driver\MongodbDriver;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Mongodb\MongodbContext;
+use Enqueue\Mongodb\MongodbDestination;
+use Enqueue\Mongodb\MongodbMessage;
+use Enqueue\Mongodb\MongodbProducer;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use Interop\Queue\Topic as InteropTopic;
+use PHPUnit\Framework\TestCase;
+
+class MongodbDriverTest extends TestCase
+{
+    use ClassExtensionTrait;
+    use GenericDriverTestsTrait;
+
+    public function testShouldImplementsDriverInterface()
+    {
+        $this->assertClassImplements(DriverInterface::class, MongodbDriver::class);
+    }
+
+    public function testShouldBeSubClassOfGenericDriver()
+    {
+        $this->assertClassExtends(GenericDriver::class, MongodbDriver::class);
+    }
+
+    public function testShouldSetupBroker()
+    {
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createCollection')
+        ;
+        $context
+            ->expects($this->once())
+            ->method('getConfig')
+            ->willReturn([
+                'dbname' => 'aDb',
+                'collection_name' => 'aCol',
+            ])
+        ;
+
+        $driver = new MongodbDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $driver->setupBroker();
+    }
+
+    protected function createDriver(...$args): DriverInterface
+    {
+        return new MongodbDriver(...$args);
+    }
+
+    /**
+     * @return MongodbContext
+     */
+    protected function createContextMock(): Context
+    {
+        return $this->createMock(MongodbContext::class);
+    }
+
+    /**
+     * @return MongodbProducer
+     */
+    protected function createProducerMock(): InteropProducer
+    {
+        return $this->createMock(MongodbProducer::class);
+    }
+
+    /**
+     * @return MongodbDestination
+     */
+    protected function createQueue(string $name): InteropQueue
+    {
+        return new MongodbDestination($name);
+    }
+
+    /**
+     * @return MongodbDestination
+     */
+    protected function createTopic(string $name): InteropTopic
+    {
+        return new MongodbDestination($name);
+    }
+
+    /**
+     * @return MongodbMessage
+     */
+    protected function createMessage(): InteropMessage
+    {
+        return new MongodbMessage();
+    }
+}
diff --git a/Tests/Client/Driver/RabbitMqDriverTest.php b/Tests/Client/Driver/RabbitMqDriverTest.php
new file mode 100644
index 0000000..b209d85
--- /dev/null
+++ b/Tests/Client/Driver/RabbitMqDriverTest.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use DMS\PHPUnitExtensions\ArraySubset\Assert;
+use Enqueue\Client\Config;
+use Enqueue\Client\Driver\AmqpDriver;
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\Driver\RabbitMqDriver;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\MessagePriority;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Amqp\AmqpContext;
+use Interop\Amqp\AmqpProducer;
+use Interop\Amqp\Impl\AmqpMessage;
+use Interop\Amqp\Impl\AmqpQueue;
+use Interop\Amqp\Impl\AmqpTopic;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use PHPUnit\Framework\TestCase;
+
+class RabbitMqDriverTest extends TestCase
+{
+    use ClassExtensionTrait;
+    use GenericDriverTestsTrait;
+
+    public function testShouldImplementsDriverInterface()
+    {
+        $this->assertClassImplements(DriverInterface::class, RabbitMqDriver::class);
+    }
+
+    public function testShouldBeSubClassOfGenericDriver()
+    {
+        $this->assertClassExtends(GenericDriver::class, RabbitMqDriver::class);
+    }
+
+    public function testShouldBeSubClassOfAmqpDriver()
+    {
+        $this->assertClassExtends(AmqpDriver::class, RabbitMqDriver::class);
+    }
+
+    public function testShouldCreateQueueWithMaxPriorityArgument()
+    {
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->willReturn($this->createQueue('aName'))
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        /** @var AmqpQueue $queue */
+        $queue = $driver->createQueue('aName');
+
+        $this->assertSame(['x-max-priority' => 4], $queue->getArguments());
+    }
+
+    protected function createDriver(...$args): DriverInterface
+    {
+        return new RabbitMqDriver(...$args);
+    }
+
+    /**
+     * @return AmqpContext
+     */
+    protected function createContextMock(): Context
+    {
+        return $this->createMock(AmqpContext::class);
+    }
+
+    /**
+     * @return AmqpProducer
+     */
+    protected function createProducerMock(): InteropProducer
+    {
+        return $this->createMock(AmqpProducer::class);
+    }
+
+    /**
+     * @return AmqpQueue
+     */
+    protected function createQueue(string $name): InteropQueue
+    {
+        return new AmqpQueue($name);
+    }
+
+    protected function createTopic(string $name): AmqpTopic
+    {
+        return new AmqpTopic($name);
+    }
+
+    /**
+     * @return AmqpMessage
+     */
+    protected function createMessage(): InteropMessage
+    {
+        return new AmqpMessage();
+    }
+
+    protected function getRouterTransportName(): string
+    {
+        return 'aprefix.router';
+    }
+
+    protected function assertTransportMessage(InteropMessage $transportMessage): void
+    {
+        $this->assertSame('body', $transportMessage->getBody());
+        Assert::assertArraySubset([
+            'hkey' => 'hval',
+            'delivery_mode' => AmqpMessage::DELIVERY_MODE_PERSISTENT,
+            'content_type' => 'ContentType',
+            'expiration' => '123000',
+            'priority' => 3,
+            'message_id' => 'theMessageId',
+            'timestamp' => 1000,
+            'reply_to' => 'theReplyTo',
+            'correlation_id' => 'theCorrelationId',
+        ], $transportMessage->getHeaders());
+        $this->assertEquals([
+            'pkey' => 'pval',
+            Config::CONTENT_TYPE => 'ContentType',
+            Config::PRIORITY => MessagePriority::HIGH,
+            Config::EXPIRE => 123,
+            Config::DELAY => 345,
+        ], $transportMessage->getProperties());
+        $this->assertSame('theMessageId', $transportMessage->getMessageId());
+        $this->assertSame(1000, $transportMessage->getTimestamp());
+        $this->assertSame('theReplyTo', $transportMessage->getReplyTo());
+        $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId());
+    }
+}
diff --git a/Tests/Client/Driver/RabbitMqStompDriverTest.php b/Tests/Client/Driver/RabbitMqStompDriverTest.php
new file mode 100644
index 0000000..9fc72be
--- /dev/null
+++ b/Tests/Client/Driver/RabbitMqStompDriverTest.php
@@ -0,0 +1,590 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\Driver\RabbitMqStompDriver;
+use Enqueue\Client\Driver\StompDriver;
+use Enqueue\Client\Driver\StompManagementClient;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Message;
+use Enqueue\Client\MessagePriority;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Stomp\ExtensionType;
+use Enqueue\Stomp\StompContext;
+use Enqueue\Stomp\StompDestination;
+use Enqueue\Stomp\StompMessage;
+use Enqueue\Stomp\StompProducer;
+use Enqueue\Test\ClassExtensionTrait;
+use Enqueue\Test\TestLogger;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use Interop\Queue\Topic as InteropTopic;
+use PHPUnit\Framework\TestCase;
+
+class RabbitMqStompDriverTest extends TestCase
+{
+    use ClassExtensionTrait;
+    use GenericDriverTestsTrait;
+
+    public function testShouldImplementsDriverInterface()
+    {
+        $this->assertClassImplements(DriverInterface::class, RabbitMqStompDriver::class);
+    }
+
+    public function testShouldBeSubClassOfGenericDriver()
+    {
+        $this->assertClassExtends(GenericDriver::class, RabbitMqStompDriver::class);
+    }
+
+    public function testShouldBeSubClassOfStompDriver()
+    {
+        $this->assertClassExtends(StompDriver::class, RabbitMqStompDriver::class);
+    }
+
+    public function testShouldCreateAndReturnStompQueueInstance()
+    {
+        $expectedQueue = new StompDestination(ExtensionType::RABBITMQ);
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with('aprefix.afooqueue')
+            ->willReturn($expectedQueue)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([]),
+            $this->createManagementClientMock()
+        );
+
+        $queue = $driver->createQueue('aFooQueue');
+
+        $expectedHeaders = [
+            'durable' => true,
+            'auto-delete' => false,
+            'exclusive' => false,
+            'x-max-priority' => 4,
+        ];
+
+        $this->assertSame($expectedQueue, $queue);
+        $this->assertTrue($queue->isDurable());
+        $this->assertFalse($queue->isAutoDelete());
+        $this->assertFalse($queue->isExclusive());
+        $this->assertSame($expectedHeaders, $queue->getHeaders());
+    }
+
+    public function testThrowIfClientPriorityInvalidOnCreateTransportMessage()
+    {
+        $clientMessage = new Message();
+        $clientMessage->setPriority('unknown');
+
+        $transportMessage = new StompMessage();
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([]),
+            $this->createManagementClientMock()
+        );
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Cant convert client priority to transport: "unknown"');
+
+        $driver->createTransportMessage($clientMessage);
+    }
+
+    public function testThrowIfDelayIsSetButDelayPluginInstalledOptionIsFalse()
+    {
+        $clientMessage = new Message();
+        $clientMessage->setDelay(123);
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn(new StompMessage())
+        ;
+
+        $config = Config::create(
+            'aPrefix',
+            '.',
+            '',
+            null,
+            null,
+            null,
+            null,
+            ['delay_plugin_installed' => false]
+        );
+
+        $driver = $this->createDriver(
+            $context,
+            $config,
+            new RouteCollection([]),
+            $this->createManagementClientMock()
+        );
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The message delaying is not supported. In order to use delay feature install RabbitMQ delay plugin.');
+
+        $driver->createTransportMessage($clientMessage);
+    }
+
+    public function testShouldSetXDelayHeaderIfDelayPluginInstalledOptionIsTrue()
+    {
+        $clientMessage = new Message();
+        $clientMessage->setDelay(123);
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn(new StompMessage())
+        ;
+
+        $config = Config::create(
+            'aPrefix',
+            '.',
+            '',
+            null,
+            null,
+            null,
+            null,
+            ['delay_plugin_installed' => true]
+        );
+
+        $driver = $this->createDriver(
+            $context,
+            $config,
+            new RouteCollection([]),
+            $this->createManagementClientMock()
+        );
+
+        $transportMessage = $driver->createTransportMessage($clientMessage);
+
+        $this->assertSame('123000', $transportMessage->getHeader('x-delay'));
+    }
+
+    public function testShouldInitDeliveryDelayIfDelayPropertyOnSendToProcessor()
+    {
+        $this->shouldSendMessageToDelayExchangeIfDelaySet();
+    }
+
+    public function shouldSendMessageToDelayExchangeIfDelaySet()
+    {
+        $queue = new StompDestination(ExtensionType::RABBITMQ);
+        $queue->setStompName('queueName');
+
+        $delayTopic = new StompDestination(ExtensionType::RABBITMQ);
+        $delayTopic->setStompName('delayTopic');
+
+        $transportMessage = new StompMessage();
+
+        $producer = $this->createProducerMock();
+        $producer
+            ->expects($this->at(0))
+            ->method('setDeliveryDelay')
+            ->with(10000)
+        ;
+        $producer
+            ->expects($this->at(1))
+            ->method('setDeliveryDelay')
+            ->with(null)
+        ;
+        $producer
+            ->expects($this->once())
+            ->method('send')
+            ->with($this->identicalTo($delayTopic), $this->identicalTo($transportMessage))
+        ;
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->willReturn($queue)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createTopic')
+            ->willReturn($delayTopic)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createProducer')
+            ->willReturn($producer)
+        ;
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($transportMessage)
+        ;
+
+        $config = Config::create(
+            'aPrefix',
+            '.',
+            '',
+            null,
+            null,
+            null,
+            null,
+            ['delay_plugin_installed' => true]
+        );
+
+        $driver = $this->createDriver(
+            $context,
+            $config,
+            new RouteCollection([
+                new Route('topic', Route::TOPIC, 'processor'),
+            ]),
+            $this->createManagementClientMock()
+        );
+
+        $message = new Message();
+        $message->setProperty(Config::TOPIC, 'topic');
+        $message->setProperty(Config::PROCESSOR, 'processor');
+        $message->setDelay(10);
+
+        $driver->sendToProcessor($message);
+    }
+
+    public function testShouldNotSetupBrokerIfManagementPluginInstalledOptionIsNotEnabled()
+    {
+        $config = Config::create(
+            'aPrefix',
+            '.',
+            '',
+            null,
+            null,
+            null,
+            null,
+            ['management_plugin_installed' => false]
+        );
+
+        $driver = $this->createDriver(
+            $this->createContextMock(),
+            $config,
+            new RouteCollection([]),
+            $this->createManagementClientMock()
+        );
+
+        $logger = new TestLogger();
+
+        $driver->setupBroker($logger);
+
+        self::assertTrue(
+            $logger->hasDebugThatContains(
+                '[RabbitMqStompDriver] Could not setup broker. The option `management_plugin_installed` is not enabled. Please enable that option and install rabbit management plugin'
+            )
+        );
+    }
+
+    public function testShouldSetupBroker()
+    {
+        $routeCollection = new RouteCollection([
+            new Route('topic', Route::TOPIC, 'processor'),
+        ]);
+
+        $managementClient = $this->createManagementClientMock();
+        $managementClient
+            ->expects($this->at(0))
+            ->method('declareExchange')
+            ->with('aprefix.router', [
+                'type' => 'fanout',
+                'durable' => true,
+                'auto_delete' => false,
+            ])
+        ;
+        $managementClient
+            ->expects($this->at(1))
+            ->method('declareQueue')
+            ->with('aprefix.default', [
+                'durable' => true,
+                'auto_delete' => false,
+                'arguments' => [
+                    'x-max-priority' => 4,
+                ],
+            ])
+        ;
+        $managementClient
+            ->expects($this->at(2))
+            ->method('bind')
+            ->with('aprefix.router', 'aprefix.default', 'aprefix.default')
+        ;
+        $managementClient
+            ->expects($this->at(3))
+            ->method('declareQueue')
+            ->with('aprefix.default', [
+                'durable' => true,
+                'auto_delete' => false,
+                'arguments' => [
+                    'x-max-priority' => 4,
+                ],
+            ])
+        ;
+
+        $contextMock = $this->createContextMock();
+        $contextMock
+            ->expects($this->any())
+            ->method('createQueue')
+            ->willReturnCallback(function (string $name) {
+                $destination = new StompDestination(ExtensionType::RABBITMQ);
+                $destination->setType(StompDestination::TYPE_QUEUE);
+                $destination->setStompName($name);
+
+                return $destination;
+            })
+        ;
+
+        $config = Config::create(
+            'aPrefix',
+            '.',
+            '',
+            null,
+            null,
+            null,
+            null,
+            ['delay_plugin_installed' => false, 'management_plugin_installed' => true]
+        );
+
+        $driver = $this->createDriver(
+            $contextMock,
+            $config,
+            $routeCollection,
+            $managementClient
+        );
+
+        $logger = new TestLogger();
+
+        $driver->setupBroker($logger);
+
+        self::assertTrue(
+            $logger->hasDebugThatContains(
+                '[RabbitMqStompDriver] Declare router exchange: aprefix.router'
+            )
+        );
+        self::assertTrue(
+            $logger->hasDebugThatContains(
+                '[RabbitMqStompDriver] Declare router queue: aprefix.default'
+            )
+        );
+        self::assertTrue(
+            $logger->hasDebugThatContains(
+                '[RabbitMqStompDriver] Bind router queue to exchange: aprefix.default -> aprefix.router'
+            )
+        );
+        self::assertTrue(
+            $logger->hasDebugThatContains(
+                '[RabbitMqStompDriver] Declare processor queue: aprefix.default'
+            )
+        );
+    }
+
+    public function testSetupBrokerShouldCreateDelayExchangeIfEnabled()
+    {
+        $routeCollection = new RouteCollection([
+            new Route('topic', Route::TOPIC, 'processor'),
+        ]);
+
+        $managementClient = $this->createManagementClientMock();
+        $managementClient
+            ->expects($this->at(4))
+            ->method('declareExchange')
+            ->with('aprefix.default.delayed', [
+                'type' => 'x-delayed-message',
+                'durable' => true,
+                'auto_delete' => false,
+                'arguments' => [
+                    'x-delayed-type' => 'direct',
+                ],
+            ])
+        ;
+        $managementClient
+            ->expects($this->at(5))
+            ->method('bind')
+            ->with('aprefix.default.delayed', 'aprefix.default', 'aprefix.default')
+        ;
+
+        $config = Config::create(
+            'aPrefix',
+            '.',
+            '',
+            null,
+            null,
+            null,
+            null,
+            ['delay_plugin_installed' => true, 'management_plugin_installed' => true]
+        );
+
+        $contextMock = $this->createContextMock();
+        $contextMock
+            ->expects($this->any())
+            ->method('createQueue')
+            ->willReturnCallback(function (string $name) {
+                $destination = new StompDestination(ExtensionType::RABBITMQ);
+                $destination->setType(StompDestination::TYPE_QUEUE);
+                $destination->setStompName($name);
+
+                return $destination;
+            })
+        ;
+        $contextMock
+            ->expects($this->any())
+            ->method('createTopic')
+            ->willReturnCallback(function (string $name) {
+                $destination = new StompDestination(ExtensionType::RABBITMQ);
+                $destination->setType(StompDestination::TYPE_TOPIC);
+                $destination->setStompName($name);
+
+                return $destination;
+            })
+        ;
+
+        $driver = $this->createDriver(
+            $contextMock,
+            $config,
+            $routeCollection,
+            $managementClient
+        );
+
+        $logger = new TestLogger();
+
+        $driver->setupBroker($logger);
+
+        self::assertTrue(
+            $logger->hasDebugThatContains(
+                '[RabbitMqStompDriver] Declare delay exchange: aprefix.default.delayed'
+            )
+        );
+        self::assertTrue(
+            $logger->hasDebugThatContains(
+                '[RabbitMqStompDriver] Bind processor queue to delay exchange: aprefix.default -> aprefix.default.delayed'
+            )
+        );
+    }
+
+    protected function createDriver(...$args): DriverInterface
+    {
+        return new RabbitMqStompDriver(
+            $args[0],
+            $args[1],
+            $args[2],
+            isset($args[3]) ? $args[3] : $this->createManagementClientMock()
+        );
+    }
+
+    /**
+     * @return StompContext
+     */
+    protected function createContextMock(): Context
+    {
+        return $this->createMock(StompContext::class);
+    }
+
+    /**
+     * @return StompProducer
+     */
+    protected function createProducerMock(): InteropProducer
+    {
+        return $this->createMock(StompProducer::class);
+    }
+
+    /**
+     * @return StompDestination
+     */
+    protected function createQueue(string $name): InteropQueue
+    {
+        $destination = new StompDestination(ExtensionType::RABBITMQ);
+        $destination->setType(StompDestination::TYPE_QUEUE);
+        $destination->setStompName($name);
+
+        return $destination;
+    }
+
+    /**
+     * @return StompDestination
+     */
+    protected function createTopic(string $name): InteropTopic
+    {
+        $destination = new StompDestination(ExtensionType::RABBITMQ);
+        $destination->setType(StompDestination::TYPE_TOPIC);
+        $destination->setStompName($name);
+
+        return $destination;
+    }
+
+    /**
+     * @return StompMessage
+     */
+    protected function createMessage(): InteropMessage
+    {
+        return new StompMessage();
+    }
+
+    protected function assertTransportMessage(InteropMessage $transportMessage): void
+    {
+        $this->assertSame('body', $transportMessage->getBody());
+        $this->assertEquals([
+            'hkey' => 'hval',
+            'message_id' => 'theMessageId',
+            'timestamp' => 1000,
+            'reply-to' => 'theReplyTo',
+            'persistent' => true,
+            'correlation_id' => 'theCorrelationId',
+            'expiration' => '123000',
+            'priority' => 3,
+            'x-delay' => '345000',
+        ], $transportMessage->getHeaders());
+        $this->assertEquals([
+            'pkey' => 'pval',
+            Config::CONTENT_TYPE => 'ContentType',
+            Config::PRIORITY => MessagePriority::HIGH,
+            Config::EXPIRE => 123,
+            Config::DELAY => 345,
+        ], $transportMessage->getProperties());
+        $this->assertSame('theMessageId', $transportMessage->getMessageId());
+        $this->assertSame(1000, $transportMessage->getTimestamp());
+        $this->assertSame('theReplyTo', $transportMessage->getReplyTo());
+        $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId());
+    }
+
+    protected function createDummyConfig(): Config
+    {
+        return Config::create(
+            'aPrefix',
+            '.',
+            '',
+            null,
+            null,
+            null,
+            null,
+            ['delay_plugin_installed' => true, 'management_plugin_installed' => true]
+        );
+    }
+
+    protected function getRouterTransportName(): string
+    {
+        return '/topic/aprefix.router';
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createManagementClientMock(): StompManagementClient
+    {
+        return $this->createMock(StompManagementClient::class);
+    }
+}
diff --git a/Tests/Client/Driver/RdKafkaDriverTest.php b/Tests/Client/Driver/RdKafkaDriverTest.php
new file mode 100644
index 0000000..c5e40e7
--- /dev/null
+++ b/Tests/Client/Driver/RdKafkaDriverTest.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\Driver\RdKafkaDriver;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\RdKafka\RdKafkaContext;
+use Enqueue\RdKafka\RdKafkaMessage;
+use Enqueue\RdKafka\RdKafkaProducer;
+use Enqueue\RdKafka\RdKafkaTopic;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use PHPUnit\Framework\TestCase;
+
+class RdKafkaDriverTest extends TestCase
+{
+    use ClassExtensionTrait;
+    use GenericDriverTestsTrait;
+
+    public function testShouldImplementsDriverInterface()
+    {
+        $this->assertClassImplements(DriverInterface::class, RdKafkaDriver::class);
+    }
+
+    public function testShouldBeSubClassOfGenericDriver()
+    {
+        $this->assertClassExtends(GenericDriver::class, RdKafkaDriver::class);
+    }
+
+    public function testShouldSetupBroker()
+    {
+        $routerTopic = new RdKafkaTopic('');
+        $routerQueue = new RdKafkaTopic('');
+
+        $processorTopic = new RdKafkaTopic('');
+
+        $context = $this->createContextMock();
+
+        $context
+            ->expects($this->at(0))
+            ->method('createQueue')
+            ->willReturn($routerTopic)
+        ;
+        $context
+            ->expects($this->at(1))
+            ->method('createQueue')
+            ->willReturn($routerQueue)
+        ;
+        $context
+            ->expects($this->at(2))
+            ->method('createQueue')
+            ->willReturn($processorTopic)
+        ;
+
+        $driver = new RdKafkaDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('topic', Route::TOPIC, 'processor'),
+            ])
+        );
+
+        $driver->setupBroker();
+    }
+
+    protected function createDriver(...$args): DriverInterface
+    {
+        return new RdKafkaDriver(...$args);
+    }
+
+    /**
+     * @return RdKafkaContext
+     */
+    protected function createContextMock(): Context
+    {
+        return $this->createMock(RdKafkaContext::class);
+    }
+
+    /**
+     * @return RdKafkaProducer
+     */
+    protected function createProducerMock(): InteropProducer
+    {
+        return $this->createMock(RdKafkaProducer::class);
+    }
+
+    /**
+     * @return RdKafkaTopic
+     */
+    protected function createQueue(string $name): InteropQueue
+    {
+        return new RdKafkaTopic($name);
+    }
+
+    protected function createTopic(string $name): RdKafkaTopic
+    {
+        return new RdKafkaTopic($name);
+    }
+
+    /**
+     * @return RdKafkaMessage
+     */
+    protected function createMessage(): InteropMessage
+    {
+        return new RdKafkaMessage();
+    }
+
+    /**
+     * @return Config
+     */
+    private function createDummyConfig()
+    {
+        return Config::create('aPrefix');
+    }
+}
diff --git a/Tests/Client/Driver/SqsDriverTest.php b/Tests/Client/Driver/SqsDriverTest.php
new file mode 100644
index 0000000..2e3005e
--- /dev/null
+++ b/Tests/Client/Driver/SqsDriverTest.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\Driver\SqsDriver;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Sqs\SqsContext;
+use Enqueue\Sqs\SqsDestination;
+use Enqueue\Sqs\SqsMessage;
+use Enqueue\Sqs\SqsProducer;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use Interop\Queue\Topic as InteropTopic;
+use PHPUnit\Framework\TestCase;
+
+class SqsDriverTest extends TestCase
+{
+    use ClassExtensionTrait;
+    use GenericDriverTestsTrait;
+
+    public function testShouldImplementsDriverInterface()
+    {
+        $this->assertClassImplements(DriverInterface::class, SqsDriver::class);
+    }
+
+    public function testShouldBeSubClassOfGenericDriver()
+    {
+        $this->assertClassExtends(GenericDriver::class, SqsDriver::class);
+    }
+
+    public function testShouldSetupBroker()
+    {
+        $routerQueue = new SqsDestination('');
+        $processorQueue = new SqsDestination('');
+
+        $context = $this->createContextMock();
+        // setup router
+        $context
+            ->expects($this->at(0))
+            ->method('createQueue')
+            ->with('aprefix_dot_default')
+            ->willReturn($routerQueue)
+        ;
+        $context
+            ->expects($this->at(1))
+            ->method('declareQueue')
+            ->with($this->identicalTo($routerQueue))
+        ;
+        // setup processor queue
+        $context
+            ->expects($this->at(2))
+            ->method('createQueue')
+            ->with('aprefix_dot_default')
+            ->willReturn($processorQueue)
+        ;
+        $context
+            ->expects($this->at(3))
+            ->method('declareQueue')
+            ->with($this->identicalTo($processorQueue))
+        ;
+
+        $driver = new SqsDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([
+                new Route('topic', Route::TOPIC, 'processor'),
+            ])
+        );
+
+        $driver->setupBroker();
+    }
+
+    protected function createDriver(...$args): DriverInterface
+    {
+        return new SqsDriver(...$args);
+    }
+
+    /**
+     * @return SqsContext
+     */
+    protected function createContextMock(): Context
+    {
+        return $this->createMock(SqsContext::class);
+    }
+
+    /**
+     * @return SqsProducer
+     */
+    protected function createProducerMock(): InteropProducer
+    {
+        return $this->createMock(SqsProducer::class);
+    }
+
+    /**
+     * @return SqsDestination
+     */
+    protected function createQueue(string $name): InteropQueue
+    {
+        return new SqsDestination($name);
+    }
+
+    /**
+     * @return SqsDestination
+     */
+    protected function createTopic(string $name): InteropTopic
+    {
+        return new SqsDestination($name);
+    }
+
+    /**
+     * @return SqsMessage
+     */
+    protected function createMessage(): InteropMessage
+    {
+        return new SqsMessage();
+    }
+
+    protected function getPrefixAppFooQueueTransportName(): string
+    {
+        return 'aprefix_dot_anappname_dot_afooqueue';
+    }
+
+    protected function getPrefixFooQueueTransportName(): string
+    {
+        return 'aprefix_dot_afooqueue';
+    }
+
+    protected function getAppFooQueueTransportName(): string
+    {
+        return 'anappname_dot_afooqueue';
+    }
+
+    protected function getDefaultQueueTransportName(): string
+    {
+        return 'aprefix_dot_default';
+    }
+
+    protected function getCustomQueueTransportName(): string
+    {
+        return 'aprefix_dot_custom';
+    }
+
+    protected function getRouterTransportName(): string
+    {
+        return 'aprefix_dot_default';
+    }
+}
diff --git a/Tests/Client/Driver/StompDriverTest.php b/Tests/Client/Driver/StompDriverTest.php
new file mode 100644
index 0000000..8f777fd
--- /dev/null
+++ b/Tests/Client/Driver/StompDriverTest.php
@@ -0,0 +1,191 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\Driver\StompDriver;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Message;
+use Enqueue\Client\MessagePriority;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Stomp\ExtensionType;
+use Enqueue\Stomp\StompContext;
+use Enqueue\Stomp\StompDestination;
+use Enqueue\Stomp\StompMessage;
+use Enqueue\Stomp\StompProducer;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Producer as InteropProducer;
+use Interop\Queue\Queue as InteropQueue;
+use Interop\Queue\Topic as InteropTopic;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+
+class StompDriverTest extends TestCase
+{
+    use ClassExtensionTrait;
+    use GenericDriverTestsTrait;
+
+    public function testShouldImplementsDriverInterface()
+    {
+        $this->assertClassImplements(DriverInterface::class, StompDriver::class);
+    }
+
+    public function testShouldBeSubClassOfGenericDriver()
+    {
+        $this->assertClassExtends(GenericDriver::class, StompDriver::class);
+    }
+
+    public function testSetupBrokerShouldOnlyLogMessageThatStompDoesNotSupportBrokerSetup()
+    {
+        $driver = new StompDriver(
+            $this->createContextMock(),
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        $logger = $this->createLoggerMock();
+        $logger
+            ->expects($this->once())
+            ->method('debug')
+            ->with('[StompDriver] Stomp protocol does not support broker configuration')
+        ;
+
+        $driver->setupBroker($logger);
+    }
+
+    public function testShouldCreateDurableQueue()
+    {
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createQueue')
+            ->willReturn($this->createQueue('aName'))
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        /** @var StompDestination $queue */
+        $queue = $driver->createQueue('aName');
+
+        $this->assertTrue($queue->isDurable());
+        $this->assertFalse($queue->isAutoDelete());
+        $this->assertFalse($queue->isExclusive());
+    }
+
+    public function testShouldSetPersistedTrueOnCreateTransportMessage()
+    {
+        $clientMessage = new Message();
+
+        $context = $this->createContextMock();
+        $context
+            ->expects($this->once())
+            ->method('createMessage')
+            ->willReturn($this->createMessage())
+        ;
+
+        $driver = $this->createDriver(
+            $context,
+            $this->createDummyConfig(),
+            new RouteCollection([])
+        );
+
+        /** @var StompMessage $transportMessage */
+        $transportMessage = $driver->createTransportMessage($clientMessage);
+
+        $this->assertTrue($transportMessage->isPersistent());
+    }
+
+    protected function createDriver(...$args): DriverInterface
+    {
+        return new StompDriver(...$args);
+    }
+
+    /**
+     * @return StompContext
+     */
+    protected function createContextMock(): Context
+    {
+        return $this->createMock(StompContext::class);
+    }
+
+    /**
+     * @return StompProducer
+     */
+    protected function createProducerMock(): InteropProducer
+    {
+        return $this->createMock(StompProducer::class);
+    }
+
+    /**
+     * @return StompDestination
+     */
+    protected function createQueue(string $name): InteropQueue
+    {
+        $destination = new StompDestination(ExtensionType::RABBITMQ);
+        $destination->setType(StompDestination::TYPE_QUEUE);
+        $destination->setStompName($name);
+
+        return $destination;
+    }
+
+    /**
+     * @return StompDestination
+     */
+    protected function createTopic(string $name): InteropTopic
+    {
+        $destination = new StompDestination(ExtensionType::RABBITMQ);
+        $destination->setType(StompDestination::TYPE_TOPIC);
+        $destination->setStompName($name);
+
+        return $destination;
+    }
+
+    /**
+     * @return StompMessage
+     */
+    protected function createMessage(): InteropMessage
+    {
+        return new StompMessage();
+    }
+
+    protected function assertTransportMessage(InteropMessage $transportMessage): void
+    {
+        $this->assertSame('body', $transportMessage->getBody());
+        $this->assertEquals([
+            'hkey' => 'hval',
+            'message_id' => 'theMessageId',
+            'timestamp' => 1000,
+            'reply-to' => 'theReplyTo',
+            'persistent' => true,
+            'correlation_id' => 'theCorrelationId',
+        ], $transportMessage->getHeaders());
+        $this->assertEquals([
+            'pkey' => 'pval',
+            Config::CONTENT_TYPE => 'ContentType',
+            Config::PRIORITY => MessagePriority::HIGH,
+            Config::EXPIRE => 123,
+            Config::DELAY => 345,
+        ], $transportMessage->getProperties());
+        $this->assertSame('theMessageId', $transportMessage->getMessageId());
+        $this->assertSame(1000, $transportMessage->getTimestamp());
+        $this->assertSame('theReplyTo', $transportMessage->getReplyTo());
+        $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId());
+    }
+
+    protected function createLoggerMock(): LoggerInterface
+    {
+        return $this->createMock(LoggerInterface::class);
+    }
+
+    protected function getRouterTransportName(): string
+    {
+        return '/topic/aprefix.router';
+    }
+}
diff --git a/Tests/Client/Driver/StompManagementClientTest.php b/Tests/Client/Driver/StompManagementClientTest.php
new file mode 100644
index 0000000..081a62c
--- /dev/null
+++ b/Tests/Client/Driver/StompManagementClientTest.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Enqueue\Tests\Client\Driver;
+
+use Enqueue\Client\Driver\StompManagementClient;
+use RabbitMq\ManagementApi\Api\Binding;
+use RabbitMq\ManagementApi\Api\Exchange;
+use RabbitMq\ManagementApi\Api\Queue;
+use RabbitMq\ManagementApi\Client;
+
+class StompManagementClientTest extends \PHPUnit\Framework\TestCase
+{
+    public function testCouldDeclareExchange()
+    {
+        $exchange = $this->createExchangeMock();
+        $exchange
+            ->expects($this->once())
+            ->method('create')
+            ->with('vhost', 'name', ['options'])
+            ->willReturn([])
+        ;
+
+        $client = $this->createClientMock();
+        $client
+            ->expects($this->once())
+            ->method('exchanges')
+            ->willReturn($exchange)
+        ;
+
+        $management = new StompManagementClient($client, 'vhost');
+        $management->declareExchange('name', ['options']);
+    }
+
+    public function testCouldDeclareQueue()
+    {
+        $queue = $this->createQueueMock();
+        $queue
+            ->expects($this->once())
+            ->method('create')
+            ->with('vhost', 'name', ['options'])
+            ->willReturn([])
+        ;
+
+        $client = $this->createClientMock();
+        $client
+            ->expects($this->once())
+            ->method('queues')
+            ->willReturn($queue)
+        ;
+
+        $management = new StompManagementClient($client, 'vhost');
+        $management->declareQueue('name', ['options']);
+    }
+
+    public function testCouldBind()
+    {
+        $binding = $this->createBindingMock();
+        $binding
+            ->expects($this->once())
+            ->method('create')
+            ->with('vhost', 'exchange', 'queue', 'routing-key', ['arguments'])
+            ->willReturn([])
+        ;
+
+        $client = $this->createClientMock();
+        $client
+            ->expects($this->once())
+            ->method('bindings')
+            ->willReturn($binding)
+        ;
+
+        $management = new StompManagementClient($client, 'vhost');
+        $management->bind('exchange', 'queue', 'routing-key', ['arguments']);
+    }
+
+    public function testCouldCreateNewInstanceUsingFactory()
+    {
+        $instance = StompManagementClient::create('', '');
+
+        $this->assertInstanceOf(StompManagementClient::class, $instance);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|Client
+     */
+    private function createClientMock()
+    {
+        return $this->createMock(Client::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|Exchange
+     */
+    private function createExchangeMock()
+    {
+        return $this->createMock(Exchange::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|Queue
+     */
+    private function createQueueMock()
+    {
+        return $this->createMock(Queue::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|Binding
+     */
+    private function createBindingMock()
+    {
+        return $this->createMock(Binding::class);
+    }
+}
diff --git a/Tests/Client/DriverFactoryTest.php b/Tests/Client/DriverFactoryTest.php
new file mode 100644
index 0000000..3d9d7b9
--- /dev/null
+++ b/Tests/Client/DriverFactoryTest.php
@@ -0,0 +1,186 @@
+<?php
+
+namespace Enqueue\Tests\Client;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\Driver\AmqpDriver;
+use Enqueue\Client\Driver\DbalDriver;
+use Enqueue\Client\Driver\FsDriver;
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\Driver\GpsDriver;
+use Enqueue\Client\Driver\MongodbDriver;
+use Enqueue\Client\Driver\RabbitMqDriver;
+use Enqueue\Client\Driver\RabbitMqStompDriver;
+use Enqueue\Client\Driver\RdKafkaDriver;
+use Enqueue\Client\Driver\SqsDriver;
+use Enqueue\Client\Driver\StompDriver;
+use Enqueue\Client\DriverFactory;
+use Enqueue\Client\DriverFactoryInterface;
+use Enqueue\Client\Resources;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Dbal\DbalConnectionFactory;
+use Enqueue\Dbal\DbalContext;
+use Enqueue\Fs\FsConnectionFactory;
+use Enqueue\Fs\FsContext;
+use Enqueue\Gearman\GearmanConnectionFactory;
+use Enqueue\Gearman\GearmanContext;
+use Enqueue\Gps\GpsConnectionFactory;
+use Enqueue\Gps\GpsContext;
+use Enqueue\Mongodb\MongodbConnectionFactory;
+use Enqueue\Mongodb\MongodbContext;
+use Enqueue\Null\NullConnectionFactory;
+use Enqueue\Null\NullContext;
+use Enqueue\Pheanstalk\PheanstalkConnectionFactory;
+use Enqueue\Pheanstalk\PheanstalkContext;
+use Enqueue\RdKafka\RdKafkaConnectionFactory;
+use Enqueue\RdKafka\RdKafkaContext;
+use Enqueue\Redis\RedisConnectionFactory;
+use Enqueue\Redis\RedisContext;
+use Enqueue\Sqs\SqsConnectionFactory;
+use Enqueue\Sqs\SqsContext;
+use Enqueue\Stomp\StompConnectionFactory;
+use Enqueue\Stomp\StompContext;
+use Interop\Amqp\AmqpConnectionFactory;
+use Interop\Amqp\AmqpContext;
+use Interop\Queue\ConnectionFactory;
+use PHPUnit\Framework\TestCase;
+
+class DriverFactoryTest extends TestCase
+{
+    public function testShouldImplementDriverFactoryInterface()
+    {
+        $rc = new \ReflectionClass(DriverFactory::class);
+
+        $this->assertTrue($rc->implementsInterface(DriverFactoryInterface::class));
+    }
+
+    public function testShouldBeFinal()
+    {
+        $rc = new \ReflectionClass(DriverFactory::class);
+
+        $this->assertTrue($rc->isFinal());
+    }
+
+    public function testThrowIfPackageThatSupportSchemeNotInstalled()
+    {
+        $scheme = 'scheme5b7aa7d7cd213';
+        $class = 'ConnectionClass5b7aa7d7cd213';
+
+        Resources::addDriver($class, [$scheme], [], ['thePackage', 'theOtherPackage']);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('To use given scheme "scheme5b7aa7d7cd213" a package has to be installed. Run "composer req thePackage theOtherPackage" to add it.');
+        $factory = new DriverFactory();
+
+        $factory->create($this->createConnectionFactoryMock(), $this->createDummyConfig($scheme.'://foo'), new RouteCollection([]));
+    }
+
+    public function testThrowIfSchemeIsNotKnown()
+    {
+        $scheme = 'scheme5b7aa862e70a5';
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('A given scheme "scheme5b7aa862e70a5" is not supported. Maybe it is a custom driver, make sure you registered it with "Enqueue\Client\Resources::addDriver".');
+
+        $factory = new DriverFactory();
+
+        $factory->create($this->createConnectionFactoryMock(), $this->createDummyConfig($scheme.'://foo'), new RouteCollection([]));
+    }
+
+    public function testThrowIfDsnInvalid()
+    {
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The DSN is invalid. It does not have scheme separator ":".');
+
+        $factory = new DriverFactory();
+
+        $factory->create($this->createConnectionFactoryMock(), $this->createDummyConfig('invalidDsn'), new RouteCollection([]));
+    }
+
+    /**
+     * @dataProvider provideDSN
+     */
+    public function testReturnsExpectedFactories(
+        string $dsn,
+        string $connectionFactoryClass,
+        string $contextClass,
+        array $conifg,
+        string $expectedDriverClass,
+    ) {
+        $connectionFactoryMock = $this->createMock($connectionFactoryClass);
+        $connectionFactoryMock
+            ->expects($this->once())
+            ->method('createContext')
+            ->willReturn($this->createMock($contextClass))
+        ;
+
+        $driverFactory = new DriverFactory();
+
+        $driver = $driverFactory->create($connectionFactoryMock, $this->createDummyConfig($dsn), new RouteCollection([]));
+
+        $this->assertInstanceOf($expectedDriverClass, $driver);
+    }
+
+    public static function provideDSN()
+    {
+        yield ['null:', NullConnectionFactory::class, NullContext::class, [], GenericDriver::class];
+
+        yield ['amqp:', AmqpConnectionFactory::class, AmqpContext::class, [], AmqpDriver::class];
+
+        yield ['amqp+rabbitmq:', AmqpConnectionFactory::class, AmqpContext::class, [], RabbitMqDriver::class];
+
+        yield ['mysql:', DbalConnectionFactory::class, DbalContext::class, [], DbalDriver::class];
+
+        yield ['file:', FsConnectionFactory::class, FsContext::class, [], FsDriver::class];
+
+        // https://github.com/php-enqueue/enqueue-dev/issues/511
+        //        yield ['gearman:', GearmanConnectionFactory::class, NullContext::class, [], NullDriver::class];
+
+        yield ['gps:', GpsConnectionFactory::class, GpsContext::class, [], GpsDriver::class];
+
+        yield ['mongodb:', MongodbConnectionFactory::class, MongodbContext::class, [], MongodbDriver::class];
+
+        yield ['kafka:', RdKafkaConnectionFactory::class, RdKafkaContext::class, [], RdKafkaDriver::class];
+
+        yield ['redis:', RedisConnectionFactory::class, RedisContext::class, [], GenericDriver::class];
+
+        yield ['redis+predis:', RedisConnectionFactory::class, RedisContext::class, [], GenericDriver::class];
+
+        yield ['sqs:', SqsConnectionFactory::class, SqsContext::class, [], SqsDriver::class];
+
+        yield ['stomp:', StompConnectionFactory::class, StompContext::class, [], StompDriver::class];
+
+        yield ['stomp+rabbitmq:', StompConnectionFactory::class, StompContext::class, [], RabbitMqStompDriver::class];
+
+        yield ['stomp+foo+bar:', StompConnectionFactory::class, StompContext::class, [], StompDriver::class];
+
+        yield ['gearman:', GearmanConnectionFactory::class, GearmanContext::class, [], GenericDriver::class];
+
+        yield ['beanstalk:', PheanstalkConnectionFactory::class, PheanstalkContext::class, [], GenericDriver::class];
+    }
+
+    private function createDummyConfig(string $dsn): Config
+    {
+        return Config::create(
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null,
+            ['dsn' => $dsn],
+            []
+        );
+    }
+
+    private function createConnectionFactoryMock(): ConnectionFactory
+    {
+        return $this->createMock(ConnectionFactory::class);
+    }
+
+    private function createConfigMock(): Config
+    {
+        return $this->createMock(Config::class);
+    }
+}
diff --git a/Tests/Client/DriverPreSendTest.php b/Tests/Client/DriverPreSendTest.php
new file mode 100644
index 0000000..32af2a8
--- /dev/null
+++ b/Tests/Client/DriverPreSendTest.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Enqueue\Tests\Client;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\DriverPreSend;
+use Enqueue\Client\Message;
+use Enqueue\Client\ProducerInterface;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+
+class DriverPreSendTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeFinal()
+    {
+        self::assertClassFinal(DriverPreSend::class);
+    }
+
+    public function testShouldAllowGetArgumentSetInConstructor()
+    {
+        $expectedMessage = new Message();
+        $expectedProducer = $this->createProducerMock();
+        $expectedDriver = $this->createDriverMock();
+
+        $context = new DriverPreSend(
+            $expectedMessage,
+            $expectedProducer,
+            $expectedDriver
+        );
+
+        $this->assertSame($expectedMessage, $context->getMessage());
+        $this->assertSame($expectedProducer, $context->getProducer());
+        $this->assertSame($expectedDriver, $context->getDriver());
+    }
+
+    public function testShouldAllowGetCommand()
+    {
+        $message = new Message();
+        $message->setProperty(Config::COMMAND, 'theCommand');
+
+        $context = new DriverPreSend(
+            $message,
+            $this->createProducerMock(),
+            $this->createDriverMock()
+        );
+
+        $this->assertFalse($context->isEvent());
+        $this->assertSame('theCommand', $context->getCommand());
+    }
+
+    public function testShouldAllowGetTopic()
+    {
+        $message = new Message();
+        $message->setProperty(Config::TOPIC, 'theTopic');
+
+        $context = new DriverPreSend(
+            $message,
+            $this->createProducerMock(),
+            $this->createDriverMock()
+        );
+
+        $this->assertTrue($context->isEvent());
+        $this->assertSame('theTopic', $context->getTopic());
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createDriverMock(): DriverInterface
+    {
+        return $this->createMock(DriverInterface::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createProducerMock(): ProducerInterface
+    {
+        return $this->createMock(ProducerInterface::class);
+    }
+}
diff --git a/Tests/Client/Extension/PrepareBodyExtensionTest.php b/Tests/Client/Extension/PrepareBodyExtensionTest.php
new file mode 100644
index 0000000..c3032cc
--- /dev/null
+++ b/Tests/Client/Extension/PrepareBodyExtensionTest.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Enqueue\Tests\Client\Extension;
+
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Extension\PrepareBodyExtension;
+use Enqueue\Client\Message;
+use Enqueue\Client\PreSend;
+use Enqueue\Client\PreSendCommandExtensionInterface;
+use Enqueue\Client\PreSendEventExtensionInterface;
+use Enqueue\Client\ProducerInterface;
+use Enqueue\Tests\Mocks\JsonSerializableObject;
+use PHPUnit\Framework\TestCase;
+
+class PrepareBodyExtensionTest extends TestCase
+{
+    public function testShouldImplementExtensionInterface()
+    {
+        $rc = new \ReflectionClass(PrepareBodyExtension::class);
+
+        $this->assertTrue($rc->implementsInterface(PreSendEventExtensionInterface::class));
+        $this->assertTrue($rc->implementsInterface(PreSendCommandExtensionInterface::class));
+    }
+
+    /**
+     * @dataProvider provideMessages
+     *
+     * @param mixed|null $contentType
+     */
+    public function testShouldSendStringUnchangedAndAddPlainTextContentTypeIfEmpty(
+        $body,
+        $contentType,
+        string $expectedBody,
+        string $expectedContentType,
+    ) {
+        $message = new Message($body);
+        $message->setContentType($contentType);
+
+        $context = $this->createDummyPreSendContext('aTopic', $message);
+
+        $extension = new PrepareBodyExtension();
+
+        $extension->onPreSendEvent($context);
+
+        $this->assertSame($expectedBody, $message->getBody());
+        $this->assertSame($expectedContentType, $message->getContentType());
+    }
+
+    public function testThrowIfBodyIsObject()
+    {
+        $message = new Message(new \stdClass());
+
+        $context = $this->createDummyPreSendContext('aTopic', $message);
+
+        $extension = new PrepareBodyExtension();
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The message\'s body must be either null, scalar, array or object (implements \JsonSerializable). Got: stdClass');
+
+        $extension->onPreSendEvent($context);
+    }
+
+    public function testThrowIfBodyIsArrayWithObjectsInsideOnSend()
+    {
+        $message = new Message(['foo' => new \stdClass()]);
+
+        $context = $this->createDummyPreSendContext('aTopic', $message);
+
+        $extension = new PrepareBodyExtension();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The message\'s body must be an array of scalars. Found not scalar in the array: stdClass');
+
+        $extension->onPreSendEvent($context);
+    }
+
+    public function testShouldThrowExceptionIfBodyIsArrayWithObjectsInSubArraysInsideOnSend()
+    {
+        $message = new Message(['foo' => ['bar' => new \stdClass()]]);
+
+        $context = $this->createDummyPreSendContext('aTopic', $message);
+
+        $extension = new PrepareBodyExtension();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The message\'s body must be an array of scalars. Found not scalar in the array: stdClass');
+
+        $extension->onPreSendEvent($context);
+    }
+
+    public static function provideMessages()
+    {
+        yield ['theBody', null, 'theBody', 'text/plain'];
+
+        yield ['theBody', 'foo/bar', 'theBody', 'foo/bar'];
+
+        yield [12345, null, '12345', 'text/plain'];
+
+        yield [12345, 'foo/bar', '12345', 'foo/bar'];
+
+        yield [12.345, null, '12.345', 'text/plain'];
+
+        yield [12.345, 'foo/bar', '12.345', 'foo/bar'];
+
+        yield [true, null, '1', 'text/plain'];
+
+        yield [true, 'foo/bar', '1', 'foo/bar'];
+
+        yield [null, null, '', 'text/plain'];
+
+        yield [null, 'foo/bar', '', 'foo/bar'];
+
+        yield [['foo' => 'fooVal'], null, '{"foo":"fooVal"}', 'application/json'];
+
+        yield [['foo' => 'fooVal'], 'foo/bar', '{"foo":"fooVal"}', 'foo/bar'];
+
+        yield [new JsonSerializableObject(), null, '{"foo":"fooVal"}', 'application/json'];
+
+        yield [new JsonSerializableObject(), 'foo/bar', '{"foo":"fooVal"}', 'foo/bar'];
+    }
+
+    private function createDummyPreSendContext($commandOrTopic, $message): PreSend
+    {
+        return new PreSend(
+            $commandOrTopic,
+            $message,
+            $this->createMock(ProducerInterface::class),
+            $this->createMock(DriverInterface::class)
+        );
+    }
+}
diff --git a/Tests/Client/Meta/QueueMetaRegistryTest.php b/Tests/Client/Meta/QueueMetaRegistryTest.php
deleted file mode 100644
index 787290f..0000000
--- a/Tests/Client/Meta/QueueMetaRegistryTest.php
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Client\Meta;
-
-use Enqueue\Client\Config;
-use Enqueue\Client\Meta\QueueMeta;
-use Enqueue\Client\Meta\QueueMetaRegistry;
-use PHPUnit\Framework\TestCase;
-
-class QueueMetaRegistryTest extends TestCase
-{
-    public function testCouldBeConstructedWithQueues()
-    {
-        $meta = [
-            'aQueueName' => [],
-            'anotherQueueName' => [],
-        ];
-
-        $registry = new QueueMetaRegistry($this->createConfig(), $meta);
-
-        $this->assertAttributeEquals($meta, 'meta', $registry);
-    }
-
-    public function testShouldAllowAddQueueMetaUsingAddMethod()
-    {
-        $registry = new QueueMetaRegistry($this->createConfig(), []);
-
-        $registry->add('theFooQueueName', 'theTransportQueueName');
-        $registry->add('theBarQueueName');
-
-        $this->assertAttributeSame([
-            'theFooQueueName' => [
-                'transportName' => 'theTransportQueueName',
-                'processors' => [],
-            ],
-            'theBarQueueName' => [
-                'transportName' => null,
-                'processors' => [],
-            ],
-        ], 'meta', $registry);
-    }
-
-    public function testShouldAllowAddSubscriber()
-    {
-        $registry = new QueueMetaRegistry($this->createConfig(), []);
-
-        $registry->addProcessor('theFooQueueName', 'theFooProcessorName');
-        $registry->addProcessor('theFooQueueName', 'theBarProcessorName');
-        $registry->addProcessor('theBarQueueName', 'theBazProcessorName');
-
-        $this->assertAttributeSame([
-            'theFooQueueName' => [
-                'transportName' => null,
-                'processors' => ['theFooProcessorName', 'theBarProcessorName'],
-            ],
-            'theBarQueueName' => [
-                'transportName' => null,
-                'processors' => ['theBazProcessorName'],
-            ],
-        ], 'meta', $registry);
-    }
-
-    public function testThrowIfThereIsNotMetaForRequestedClientQueueName()
-    {
-        $registry = new QueueMetaRegistry($this->createConfig(), []);
-
-        $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionMessage('The queue meta not found. Requested name `aName`');
-        $registry->getQueueMeta('aName');
-    }
-
-    public function testShouldAllowGetQueueByNameWithDefaultInfo()
-    {
-        $queues = [
-            'theQueueName' => [],
-        ];
-
-        $registry = new QueueMetaRegistry($this->createConfig(), $queues);
-
-        $queue = $registry->getQueueMeta('theQueueName');
-
-        $this->assertInstanceOf(QueueMeta::class, $queue);
-        $this->assertSame('theQueueName', $queue->getClientName());
-        $this->assertSame('aprefix.anappname.thequeuename', $queue->getTransportName());
-        $this->assertSame([], $queue->getProcessors());
-    }
-
-    public function testShouldAllowGetQueueByNameWithCustomInfo()
-    {
-        $queues = [
-            'theClientQueueName' => ['transportName' => 'theTransportName', 'processors' => ['theSubscriber']],
-        ];
-
-        $registry = new QueueMetaRegistry($this->createConfig(), $queues);
-
-        $queue = $registry->getQueueMeta('theClientQueueName');
-        $this->assertInstanceOf(QueueMeta::class, $queue);
-        $this->assertSame('theClientQueueName', $queue->getClientName());
-        $this->assertSame('theTransportName', $queue->getTransportName());
-        $this->assertSame(['theSubscriber'], $queue->getProcessors());
-    }
-
-    public function testShouldNotAllowToOverwriteDefaultTransportNameByEmptyValue()
-    {
-        $registry = new QueueMetaRegistry($this->createConfig(), [
-            'theClientQueueName' => ['transportName' => null, 'processors' => []],
-        ]);
-
-        $queue = $registry->getQueueMeta('theClientQueueName');
-        $this->assertInstanceOf(QueueMeta::class, $queue);
-        $this->assertSame('aprefix.anappname.theclientqueuename', $queue->getTransportName());
-    }
-
-    public function testShouldAllowGetAllQueues()
-    {
-        $queues = [
-            'fooQueueName' => [],
-            'barQueueName' => [],
-        ];
-
-        $registry = new QueueMetaRegistry($this->createConfig(), $queues);
-
-        $queues = $registry->getQueuesMeta();
-        $this->assertInstanceOf(\Generator::class, $queues);
-
-        $queues = iterator_to_array($queues);
-        /* @var QueueMeta[] $queues */
-
-        $this->assertContainsOnly(QueueMeta::class, $queues);
-        $this->assertCount(2, $queues);
-
-        $this->assertSame('fooQueueName', $queues[0]->getClientName());
-        $this->assertSame('aprefix.anappname.fooqueuename', $queues[0]->getTransportName());
-
-        $this->assertSame('barQueueName', $queues[1]->getClientName());
-        $this->assertSame('aprefix.anappname.barqueuename', $queues[1]->getTransportName());
-    }
-
-    /**
-     * @return Config
-     */
-    private function createConfig()
-    {
-        return new Config('aPrefix', 'anAppName', 'aRouterTopic', 'aRouterQueueName', 'aDefaultQueueName', 'aRouterProcessorName');
-    }
-}
diff --git a/Tests/Client/Meta/QueueMetaTest.php b/Tests/Client/Meta/QueueMetaTest.php
deleted file mode 100644
index fde6dc5..0000000
--- a/Tests/Client/Meta/QueueMetaTest.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Client\Meta;
-
-use Enqueue\Client\Meta\QueueMeta;
-use PHPUnit\Framework\TestCase;
-
-class QueueMetaTest extends TestCase
-{
-    public function testCouldBeConstructedWithExpectedArguments()
-    {
-        $meta = new QueueMeta('aClientName', 'aTransportName');
-
-        $this->assertAttributeEquals('aClientName', 'clientName', $meta);
-        $this->assertAttributeEquals('aTransportName', 'transportName', $meta);
-        $this->assertAttributeEquals([], 'processors', $meta);
-    }
-
-    public function testShouldAllowGetClientNameSetInConstructor()
-    {
-        $meta = new QueueMeta('theClientName', 'aTransportName');
-
-        $this->assertSame('theClientName', $meta->getClientName());
-    }
-
-    public function testShouldAllowGetTransportNameSetInConstructor()
-    {
-        $meta = new QueueMeta('aClientName', 'theTransportName');
-
-        $this->assertSame('theTransportName', $meta->getTransportName());
-    }
-
-    public function testShouldAllowGetSubscribersSetInConstructor()
-    {
-        $meta = new QueueMeta('aClientName', 'aTransportName', ['aSubscriber']);
-
-        $this->assertSame(['aSubscriber'], $meta->getProcessors());
-    }
-}
diff --git a/Tests/Client/Meta/TopicMetaRegistryTest.php b/Tests/Client/Meta/TopicMetaRegistryTest.php
deleted file mode 100644
index ce074b6..0000000
--- a/Tests/Client/Meta/TopicMetaRegistryTest.php
+++ /dev/null
@@ -1,124 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Client\Meta;
-
-use Enqueue\Client\Meta\TopicMeta;
-use Enqueue\Client\Meta\TopicMetaRegistry;
-use PHPUnit\Framework\TestCase;
-
-class TopicMetaRegistryTest extends TestCase
-{
-    public function testCouldBeConstructedWithTopics()
-    {
-        $topics = [
-            'aTopicName' => [],
-            'anotherTopicName' => [],
-        ];
-
-        $registry = new TopicMetaRegistry($topics);
-
-        $this->assertAttributeEquals($topics, 'meta', $registry);
-    }
-
-    public function testShouldAllowAddTopicMetaUsingAddMethod()
-    {
-        $registry = new TopicMetaRegistry([]);
-
-        $registry->add('theFooTopicName', 'aDescription');
-        $registry->add('theBarTopicName');
-
-        $this->assertAttributeSame([
-            'theFooTopicName' => [
-                'description' => 'aDescription',
-                'processors' => [],
-            ],
-            'theBarTopicName' => [
-                'description' => null,
-                'processors' => [],
-            ],
-        ], 'meta', $registry);
-    }
-
-    public function testShouldAllowAddSubscriber()
-    {
-        $registry = new TopicMetaRegistry([]);
-
-        $registry->addProcessor('theFooTopicName', 'theFooProcessorName');
-        $registry->addProcessor('theFooTopicName', 'theBarProcessorName');
-        $registry->addProcessor('theBarTopicName', 'theBazProcessorName');
-
-        $this->assertAttributeSame([
-            'theFooTopicName' => [
-                'description' => null,
-                'processors' => ['theFooProcessorName', 'theBarProcessorName'],
-            ],
-            'theBarTopicName' => [
-                'description' => null,
-                'processors' => ['theBazProcessorName'],
-            ],
-        ], 'meta', $registry);
-    }
-
-    public function testThrowIfThereIsNotMetaForRequestedTopicName()
-    {
-        $registry = new TopicMetaRegistry([]);
-
-        $this->setExpectedException(
-            \InvalidArgumentException::class,
-            'The topic meta not found. Requested name `aName`'
-        );
-        $registry->getTopicMeta('aName');
-    }
-
-    public function testShouldAllowGetTopicByNameWithDefaultInfo()
-    {
-        $topics = [
-            'theTopicName' => [],
-        ];
-
-        $registry = new TopicMetaRegistry($topics);
-
-        $topic = $registry->getTopicMeta('theTopicName');
-        $this->assertInstanceOf(TopicMeta::class, $topic);
-        $this->assertSame('theTopicName', $topic->getName());
-        $this->assertSame('', $topic->getDescription());
-        $this->assertSame([], $topic->getProcessors());
-    }
-
-    public function testShouldAllowGetTopicByNameWithCustomInfo()
-    {
-        $topics = [
-            'theTopicName' => ['description' => 'theDescription', 'processors' => ['theSubscriber']],
-        ];
-
-        $registry = new TopicMetaRegistry($topics);
-
-        $topic = $registry->getTopicMeta('theTopicName');
-        $this->assertInstanceOf(TopicMeta::class, $topic);
-        $this->assertSame('theTopicName', $topic->getName());
-        $this->assertSame('theDescription', $topic->getDescription());
-        $this->assertSame(['theSubscriber'], $topic->getProcessors());
-    }
-
-    public function testShouldAllowGetAllTopics()
-    {
-        $topics = [
-            'fooTopicName' => [],
-            'barTopicName' => [],
-        ];
-
-        $registry = new TopicMetaRegistry($topics);
-
-        $topics = $registry->getTopicsMeta();
-        $this->assertInstanceOf(\Generator::class, $topics);
-
-        $topics = iterator_to_array($topics);
-        /* @var TopicMeta[] $topics */
-
-        $this->assertContainsOnly(TopicMeta::class, $topics);
-        $this->assertCount(2, $topics);
-
-        $this->assertSame('fooTopicName', $topics[0]->getName());
-        $this->assertSame('barTopicName', $topics[1]->getName());
-    }
-}
diff --git a/Tests/Client/Meta/TopicMetaTest.php b/Tests/Client/Meta/TopicMetaTest.php
deleted file mode 100644
index 565a8f8..0000000
--- a/Tests/Client/Meta/TopicMetaTest.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Client\Meta;
-
-use Enqueue\Client\Meta\TopicMeta;
-use PHPUnit\Framework\TestCase;
-
-class TopicMetaTest extends TestCase
-{
-    public function testCouldBeConstructedWithNameOnly()
-    {
-        $topic = new TopicMeta('aName');
-
-        $this->assertAttributeEquals('aName', 'name', $topic);
-        $this->assertAttributeEquals('', 'description', $topic);
-        $this->assertAttributeEquals([], 'processors', $topic);
-    }
-
-    public function testCouldBeConstructedWithNameAndDescriptionOnly()
-    {
-        $topic = new TopicMeta('aName', 'aDescription');
-
-        $this->assertAttributeEquals('aName', 'name', $topic);
-        $this->assertAttributeEquals('aDescription', 'description', $topic);
-        $this->assertAttributeEquals([], 'processors', $topic);
-    }
-
-    public function testCouldBeConstructedWithNameAndDescriptionAndSubscribers()
-    {
-        $topic = new TopicMeta('aName', 'aDescription', ['aSubscriber']);
-
-        $this->assertAttributeEquals('aName', 'name', $topic);
-        $this->assertAttributeEquals('aDescription', 'description', $topic);
-        $this->assertAttributeEquals(['aSubscriber'], 'processors', $topic);
-    }
-
-    public function testShouldAllowGetNameSetInConstructor()
-    {
-        $topic = new TopicMeta('theName', 'aDescription');
-
-        $this->assertSame('theName', $topic->getName());
-    }
-
-    public function testShouldAllowGetDescriptionSetInConstructor()
-    {
-        $topic = new TopicMeta('aName', 'theDescription');
-
-        $this->assertSame('theDescription', $topic->getDescription());
-    }
-
-    public function testShouldAllowGetSubscribersSetInConstructor()
-    {
-        $topic = new TopicMeta('aName', '', ['aSubscriber']);
-
-        $this->assertSame(['aSubscriber'], $topic->getProcessors());
-    }
-}
diff --git a/Tests/Client/PostSendTest.php b/Tests/Client/PostSendTest.php
new file mode 100644
index 0000000..ba51710
--- /dev/null
+++ b/Tests/Client/PostSendTest.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Enqueue\Tests\Client;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Message;
+use Enqueue\Client\PostSend;
+use Enqueue\Client\ProducerInterface;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as TransportMessage;
+use PHPUnit\Framework\TestCase;
+
+class PostSendTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeFinal()
+    {
+        self::assertClassFinal(PostSend::class);
+    }
+
+    public function testShouldAllowGetArgumentSetInConstructor()
+    {
+        $expectedMessage = new Message();
+        $expectedProducer = $this->createProducerMock();
+        $expectedDriver = $this->createDriverMock();
+        $expectedDestination = $this->createDestinationMock();
+        $expectedTransportMessage = $this->createTransportMessageMock();
+
+        $context = new PostSend(
+            $expectedMessage,
+            $expectedProducer,
+            $expectedDriver,
+            $expectedDestination,
+            $expectedTransportMessage
+        );
+
+        $this->assertSame($expectedMessage, $context->getMessage());
+        $this->assertSame($expectedProducer, $context->getProducer());
+        $this->assertSame($expectedDriver, $context->getDriver());
+        $this->assertSame($expectedDestination, $context->getTransportDestination());
+        $this->assertSame($expectedTransportMessage, $context->getTransportMessage());
+    }
+
+    public function testShouldAllowGetCommand()
+    {
+        $message = new Message();
+        $message->setProperty(Config::COMMAND, 'theCommand');
+
+        $context = new PostSend(
+            $message,
+            $this->createProducerMock(),
+            $this->createDriverMock(),
+            $this->createDestinationMock(),
+            $this->createTransportMessageMock()
+        );
+
+        $this->assertFalse($context->isEvent());
+        $this->assertSame('theCommand', $context->getCommand());
+    }
+
+    public function testShouldAllowGetTopic()
+    {
+        $message = new Message();
+        $message->setProperty(Config::TOPIC, 'theTopic');
+
+        $context = new PostSend(
+            $message,
+            $this->createProducerMock(),
+            $this->createDriverMock(),
+            $this->createDestinationMock(),
+            $this->createTransportMessageMock()
+        );
+
+        $this->assertTrue($context->isEvent());
+        $this->assertSame('theTopic', $context->getTopic());
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createDriverMock(): DriverInterface
+    {
+        return $this->createMock(DriverInterface::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createProducerMock(): ProducerInterface
+    {
+        return $this->createMock(ProducerInterface::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|Destination
+     */
+    private function createDestinationMock(): Destination
+    {
+        return $this->createMock(Destination::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|TransportMessage
+     */
+    private function createTransportMessageMock(): TransportMessage
+    {
+        return $this->createMock(TransportMessage::class);
+    }
+}
diff --git a/Tests/Client/PreSendTest.php b/Tests/Client/PreSendTest.php
new file mode 100644
index 0000000..01a7e50
--- /dev/null
+++ b/Tests/Client/PreSendTest.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Enqueue\Tests\Client;
+
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Message;
+use Enqueue\Client\PreSend;
+use Enqueue\Client\ProducerInterface;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+
+class PreSendTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeFinal()
+    {
+        self::assertClassFinal(PreSend::class);
+    }
+
+    public function testShouldAllowGetArgumentSetInConstructor()
+    {
+        $expectedCommandOrTopic = 'theCommandOrTopic';
+        $expectedMessage = new Message();
+        $expectedProducer = $this->createProducerMock();
+        $expectedDriver = $this->createDriverMock();
+
+        $context = new PreSend(
+            $expectedCommandOrTopic,
+            $expectedMessage,
+            $expectedProducer,
+            $expectedDriver
+        );
+
+        $this->assertSame($expectedCommandOrTopic, $context->getTopic());
+        $this->assertSame($expectedCommandOrTopic, $context->getCommand());
+        $this->assertSame($expectedMessage, $context->getMessage());
+        $this->assertSame($expectedProducer, $context->getProducer());
+        $this->assertSame($expectedDriver, $context->getDriver());
+
+        $this->assertEquals($expectedMessage, $context->getOriginalMessage());
+        $this->assertNotSame($expectedMessage, $context->getOriginalMessage());
+    }
+
+    public function testCouldChangeTopic()
+    {
+        $context = new PreSend(
+            'aCommandOrTopic',
+            new Message(),
+            $this->createProducerMock(),
+            $this->createDriverMock()
+        );
+
+        // guard
+        $this->assertSame('aCommandOrTopic', $context->getTopic());
+
+        $context->changeTopic('theChangedTopic');
+
+        $this->assertSame('theChangedTopic', $context->getTopic());
+    }
+
+    public function testCouldChangeCommand()
+    {
+        $context = new PreSend(
+            'aCommandOrTopic',
+            new Message(),
+            $this->createProducerMock(),
+            $this->createDriverMock()
+        );
+
+        // guard
+        $this->assertSame('aCommandOrTopic', $context->getCommand());
+
+        $context->changeCommand('theChangedCommand');
+
+        $this->assertSame('theChangedCommand', $context->getCommand());
+    }
+
+    public function testCouldChangeBody()
+    {
+        $context = new PreSend(
+            'aCommandOrTopic',
+            new Message('aBody'),
+            $this->createProducerMock(),
+            $this->createDriverMock()
+        );
+
+        // guard
+        $this->assertSame('aBody', $context->getMessage()->getBody());
+        $this->assertNull($context->getMessage()->getContentType());
+
+        $context->changeBody('theChangedBody');
+        $this->assertSame('theChangedBody', $context->getMessage()->getBody());
+        $this->assertNull($context->getMessage()->getContentType());
+
+        $context->changeBody('theChangedBodyAgain', 'foo/bar');
+        $this->assertSame('theChangedBodyAgain', $context->getMessage()->getBody());
+        $this->assertSame('foo/bar', $context->getMessage()->getContentType());
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createDriverMock(): DriverInterface
+    {
+        return $this->createMock(DriverInterface::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createProducerMock(): ProducerInterface
+    {
+        return $this->createMock(ProducerInterface::class);
+    }
+}
diff --git a/Tests/Client/ProducerSendCommandTest.php b/Tests/Client/ProducerSendCommandTest.php
new file mode 100644
index 0000000..9500e9d
--- /dev/null
+++ b/Tests/Client/ProducerSendCommandTest.php
@@ -0,0 +1,537 @@
+<?php
+
+namespace Enqueue\Tests\Client;
+
+use Enqueue\Client\ChainExtension;
+use Enqueue\Client\Config;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\DriverPreSend;
+use Enqueue\Client\DriverSendResult;
+use Enqueue\Client\ExtensionInterface;
+use Enqueue\Client\Message;
+use Enqueue\Client\MessagePriority;
+use Enqueue\Client\PostSend;
+use Enqueue\Client\PreSend;
+use Enqueue\Client\Producer;
+use Enqueue\Rpc\Promise;
+use Enqueue\Rpc\RpcFactory;
+use Enqueue\Test\ClassExtensionTrait;
+use Enqueue\Tests\Mocks\CustomPrepareBodyClientExtension;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as TransportMessage;
+use PHPUnit\Framework\TestCase;
+
+class ProducerSendCommandTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldSendCommandToProcessor()
+    {
+        $message = new Message();
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+        $driver
+            ->expects($this->never())
+            ->method('sendToRouter')
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendCommand('command', $message);
+
+        $expectedProperties = [
+            'enqueue.command' => 'command',
+        ];
+
+        self::assertEquals($expectedProperties, $message->getProperties());
+    }
+
+    public function testShouldSendCommandWithReply()
+    {
+        $message = new Message();
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+        $driver
+            ->expects($this->never())
+            ->method('sendToRouter')
+        ;
+
+        $expectedPromiseMock = $this->createMock(Promise::class);
+
+        $rpcFactoryMock = $this->createRpcFactoryMock();
+        $rpcFactoryMock
+            ->expects($this->once())
+            ->method('createReplyTo')
+            ->willReturn('theReplyQueue')
+        ;
+        $rpcFactoryMock
+            ->expects($this->once())
+            ->method('createPromise')
+            ->with(
+                'theReplyQueue',
+                $this->logicalNot($this->isEmpty()),
+                60000
+            )
+            ->willReturn($expectedPromiseMock)
+        ;
+
+        $producer = new Producer($driver, $rpcFactoryMock);
+        $actualPromise = $producer->sendCommand('command', $message, true);
+
+        $this->assertSame($expectedPromiseMock, $actualPromise);
+
+        self::assertEquals('theReplyQueue', $message->getReplyTo());
+        self::assertNotEmpty($message->getCorrelationId());
+    }
+
+    public function testShouldSendCommandWithReplyAndCustomReplyQueueAndCorrelationId()
+    {
+        $message = new Message();
+        $message->setReplyTo('theCustomReplyQueue');
+        $message->setCorrelationId('theCustomCorrelationId');
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+        $driver
+            ->expects($this->never())
+            ->method('sendToRouter')
+        ;
+
+        $expectedPromiseMock = $this->createMock(Promise::class);
+
+        $rpcFactoryMock = $this->createRpcFactoryMock();
+        $rpcFactoryMock
+            ->expects($this->never())
+            ->method('createReplyTo')
+        ;
+        $rpcFactoryMock
+            ->expects($this->once())
+            ->method('createPromise')
+            ->with(
+                'theCustomReplyQueue',
+                'theCustomCorrelationId',
+                60000
+            )
+            ->willReturn($expectedPromiseMock)
+        ;
+
+        $producer = new Producer($driver, $rpcFactoryMock);
+        $actualPromise = $producer->sendCommand('command', $message, true);
+
+        $this->assertSame($expectedPromiseMock, $actualPromise);
+
+        self::assertEquals('theCustomReplyQueue', $message->getReplyTo());
+        self::assertSame('theCustomCorrelationId', $message->getCorrelationId());
+    }
+
+    public function testShouldOverwriteExpectedMessageProperties()
+    {
+        $message = new Message();
+        $message->setProperty(Config::COMMAND, 'commandShouldBeOverwritten');
+        $message->setScope('scopeShouldBeOverwritten');
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendCommand('expectedCommand', $message);
+
+        $expectedProperties = [
+            'enqueue.command' => 'expectedCommand',
+        ];
+
+        self::assertEquals($expectedProperties, $message->getProperties());
+        self::assertSame(Message::SCOPE_APP, $message->getScope());
+    }
+
+    public function testShouldSendCommandWithoutPriorityByDefault()
+    {
+        $message = new Message();
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendCommand('command', $message);
+
+        self::assertNull($message->getPriority());
+    }
+
+    public function testShouldSendCommandWithCustomPriority()
+    {
+        $message = new Message();
+        $message->setPriority(MessagePriority::HIGH);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendCommand('command', $message);
+
+        self::assertSame(MessagePriority::HIGH, $message->getPriority());
+    }
+
+    public function testShouldSendCommandWithGeneratedMessageId()
+    {
+        $message = new Message();
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendCommand('command', $message);
+
+        self::assertNotEmpty($message->getMessageId());
+    }
+
+    public function testShouldSendCommandWithCustomMessageId()
+    {
+        $message = new Message();
+        $message->setMessageId('theCustomMessageId');
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendCommand('command', $message);
+
+        self::assertSame('theCustomMessageId', $message->getMessageId());
+    }
+
+    public function testShouldSendCommandWithGeneratedTimestamp()
+    {
+        $message = new Message();
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendCommand('command', $message);
+
+        self::assertNotEmpty($message->getTimestamp());
+    }
+
+    public function testShouldSendCommandWithCustomTimestamp()
+    {
+        $message = new Message();
+        $message->setTimestamp('theCustomTimestamp');
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendCommand('command', $message);
+
+        self::assertSame('theCustomTimestamp', $message->getTimestamp());
+    }
+
+    public function testShouldSerializeMessageToJsonByDefault()
+    {
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->willReturnCallback(function (Message $message) {
+                $this->assertSame('{"foo":"fooVal"}', $message->getBody());
+
+                return $this->createDriverSendResult();
+            })
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendCommand('command', ['foo' => 'fooVal']);
+    }
+
+    public function testShouldSerializeMessageByCustomExtension()
+    {
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->willReturnCallback(function (Message $message) {
+                $this->assertSame('theCommandBodySerializedByCustomExtension', $message->getBody());
+
+                return $this->createDriverSendResult();
+            })
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock(), new ChainExtension([new CustomPrepareBodyClientExtension()]));
+        $producer->sendCommand('command', ['foo' => 'fooVal']);
+    }
+
+    public function testShouldSendCommandToApplicationRouter()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_APP);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->willReturnCallback(function (Message $message) {
+                self::assertSame('aBody', $message->getBody());
+                self::assertNull($message->getProperty(Config::PROCESSOR));
+                self::assertSame('command', $message->getProperty(Config::COMMAND));
+
+                return $this->createDriverSendResult();
+            })
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendCommand('command', $message);
+    }
+
+    public function testThrowWhenProcessorNamePropertySetToApplicationRouter()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_APP);
+        $message->setProperty(Config::PROCESSOR, 'aCustomProcessor');
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->never())
+            ->method('sendToProcessor')
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The enqueue.processor property must not be set.');
+        $producer->sendCommand('command', $message);
+    }
+
+    public function testShouldCallPreSendCommandExtensionMethodWhenSendToBus()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_MESSAGE_BUS);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $extension = $this->createMock(ExtensionInterface::class);
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension);
+
+        $extension
+            ->expects($this->at(0))
+            ->method('onPreSendCommand')
+            ->willReturnCallback(function (PreSend $context) use ($message, $producer, $driver) {
+                $this->assertSame($message, $context->getMessage());
+                $this->assertSame($producer, $context->getProducer());
+                $this->assertSame($driver, $context->getDriver());
+                $this->assertSame('command', $context->getCommand());
+
+                $this->assertEquals($message, $context->getOriginalMessage());
+                $this->assertNotSame($message, $context->getOriginalMessage());
+            });
+
+        $extension
+            ->expects($this->never())
+            ->method('onPreSendEvent')
+        ;
+
+        $producer->sendCommand('command', $message);
+    }
+
+    public function testShouldCallPreSendCommandExtensionMethodWhenSendToApplicationRouter()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_APP);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $extension = $this->createMock(ExtensionInterface::class);
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension);
+
+        $extension
+            ->expects($this->at(0))
+            ->method('onPreSendCommand')
+            ->willReturnCallback(function (PreSend $context) use ($message, $producer, $driver) {
+                $this->assertSame($message, $context->getMessage());
+                $this->assertSame($producer, $context->getProducer());
+                $this->assertSame($driver, $context->getDriver());
+                $this->assertSame('command', $context->getCommand());
+
+                $this->assertEquals($message, $context->getOriginalMessage());
+                $this->assertNotSame($message, $context->getOriginalMessage());
+            });
+
+        $extension
+            ->expects($this->never())
+            ->method('onPreSendEvent')
+        ;
+
+        $producer->sendCommand('command', $message);
+    }
+
+    public function testShouldCallPreDriverSendExtensionMethod()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_APP);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $extension = $this->createMock(ExtensionInterface::class);
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension);
+
+        $extension
+            ->expects($this->at(0))
+            ->method('onDriverPreSend')
+            ->willReturnCallback(function (DriverPreSend $context) use ($message, $producer, $driver) {
+                $this->assertSame($message, $context->getMessage());
+                $this->assertSame($producer, $context->getProducer());
+                $this->assertSame($driver, $context->getDriver());
+                $this->assertSame('command', $context->getCommand());
+
+                $this->assertTrue($context->isEvent());
+            });
+
+        $producer->sendCommand('command', $message);
+    }
+
+    public function testShouldCallPostSendExtensionMethod()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_APP);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $extension = $this->createMock(ExtensionInterface::class);
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension);
+
+        $extension
+            ->expects($this->at(0))
+            ->method('onPostSend')
+            ->willReturnCallback(function (PostSend $context) use ($message, $producer, $driver) {
+                $this->assertSame($message, $context->getMessage());
+                $this->assertSame($producer, $context->getProducer());
+                $this->assertSame($driver, $context->getDriver());
+                $this->assertSame('command', $context->getCommand());
+
+                $this->assertFalse($context->isEvent());
+            });
+
+        $producer->sendCommand('command', $message);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createRpcFactoryMock(): RpcFactory
+    {
+        return $this->createMock(RpcFactory::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createDriverStub(): DriverInterface
+    {
+        $config = new Config(
+            'a_prefix',
+            '.',
+            'an_app',
+            'a_router_topic',
+            'a_router_queue',
+            'a_default_processor_queue',
+            'a_router_processor_name',
+            [],
+            []
+        );
+
+        $driverMock = $this->createMock(DriverInterface::class);
+        $driverMock
+            ->expects($this->any())
+            ->method('getConfig')
+            ->willReturn($config)
+        ;
+
+        return $driverMock;
+    }
+
+    private function createDriverSendResult(): DriverSendResult
+    {
+        return new DriverSendResult(
+            $this->createMock(Destination::class),
+            $this->createMock(TransportMessage::class)
+        );
+    }
+}
diff --git a/Tests/Client/ProducerSendEventTest.php b/Tests/Client/ProducerSendEventTest.php
new file mode 100644
index 0000000..c92b495
--- /dev/null
+++ b/Tests/Client/ProducerSendEventTest.php
@@ -0,0 +1,557 @@
+<?php
+
+namespace Enqueue\Tests\Client;
+
+use Enqueue\Client\ChainExtension;
+use Enqueue\Client\Config;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\DriverPreSend;
+use Enqueue\Client\DriverSendResult;
+use Enqueue\Client\ExtensionInterface;
+use Enqueue\Client\Message;
+use Enqueue\Client\MessagePriority;
+use Enqueue\Client\PostSend;
+use Enqueue\Client\PreSend;
+use Enqueue\Client\Producer;
+use Enqueue\Rpc\RpcFactory;
+use Enqueue\Tests\Mocks\CustomPrepareBodyClientExtension;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as TransportMessage;
+use PHPUnit\Framework\TestCase;
+
+class ProducerSendEventTest extends TestCase
+{
+    public function testShouldSendEventToRouter()
+    {
+        $message = new Message();
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+        $driver
+            ->expects($this->never())
+            ->method('sendToProcessor')
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendEvent('topic', $message);
+
+        $expectedProperties = [
+            'enqueue.topic' => 'topic',
+        ];
+
+        self::assertEquals($expectedProperties, $message->getProperties());
+    }
+
+    public function testShouldOverwriteTopicProperty()
+    {
+        $message = new Message();
+        $message->setProperty(Config::TOPIC, 'topicShouldBeOverwritten');
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendEvent('expectedTopic', $message);
+
+        $expectedProperties = [
+            'enqueue.topic' => 'expectedTopic',
+        ];
+
+        self::assertEquals($expectedProperties, $message->getProperties());
+    }
+
+    public function testShouldSendEventWithoutPriorityByDefault()
+    {
+        $message = new Message();
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendEvent('topic', $message);
+
+        self::assertNull($message->getPriority());
+    }
+
+    public function testShouldSendEventWithCustomPriority()
+    {
+        $message = new Message();
+        $message->setPriority(MessagePriority::HIGH);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendEvent('topic', $message);
+
+        self::assertSame(MessagePriority::HIGH, $message->getPriority());
+    }
+
+    public function testShouldSendEventWithGeneratedMessageId()
+    {
+        $message = new Message();
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendEvent('topic', $message);
+
+        self::assertNotEmpty($message->getMessageId());
+    }
+
+    public function testShouldSendEventWithCustomMessageId()
+    {
+        $message = new Message();
+        $message->setMessageId('theCustomMessageId');
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendEvent('topic', $message);
+
+        self::assertSame('theCustomMessageId', $message->getMessageId());
+    }
+
+    public function testShouldSendEventWithGeneratedTimestamp()
+    {
+        $message = new Message();
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendEvent('topic', $message);
+
+        self::assertNotEmpty($message->getTimestamp());
+    }
+
+    public function testShouldSendEventWithCustomTimestamp()
+    {
+        $message = new Message();
+        $message->setTimestamp('theCustomTimestamp');
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->with(self::identicalTo($message))
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendEvent('topic', $message);
+
+        self::assertSame('theCustomTimestamp', $message->getTimestamp());
+    }
+
+    public function testShouldSerializeMessageToJsonByDefault()
+    {
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->willReturnCallback(function (Message $message) {
+                $this->assertSame('{"foo":"fooVal"}', $message->getBody());
+
+                return $this->createDriverSendResult();
+            })
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendEvent('topic', ['foo' => 'fooVal']);
+    }
+
+    public function testShouldSerializeMessageByCustomExtension()
+    {
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->willReturnCallback(function (Message $message) {
+                $this->assertSame('theEventBodySerializedByCustomExtension', $message->getBody());
+
+                return $this->createDriverSendResult();
+            })
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock(), new ChainExtension([new CustomPrepareBodyClientExtension()]));
+        $producer->sendEvent('topic', ['foo' => 'fooVal']);
+    }
+
+    public function testThrowIfSendEventToMessageBusWithProcessorNamePropertySet()
+    {
+        $message = new Message();
+        $message->setBody('');
+        $message->setProperty(Config::PROCESSOR, 'aProcessor');
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->never())
+            ->method('sendToRouter')
+        ;
+        $driver
+            ->expects($this->never())
+            ->method('sendToProcessor')
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The enqueue.processor property must not be set.');
+        $producer->sendEvent('topic', $message);
+    }
+
+    public function testShouldSendEventToApplicationRouter()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_APP);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->never())
+            ->method('sendToRouter')
+        ;
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->willReturnCallback(function (Message $message) {
+                self::assertSame('aBody', $message->getBody());
+
+                // null means a driver sends a message to router processor.
+                self::assertNull($message->getProperty(Config::PROCESSOR));
+
+                return $this->createDriverSendResult();
+            })
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+        $producer->sendEvent('topic', $message);
+    }
+
+    public function testThrowWhenProcessorNamePropertySetToApplicationRouter()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_APP);
+        $message->setProperty(Config::PROCESSOR, 'aCustomProcessor');
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->never())
+            ->method('sendToProcessor')
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The enqueue.processor property must not be set.');
+        $producer->sendEvent('topic', $message);
+    }
+
+    public function testThrowIfUnSupportedScopeGivenOnSend()
+    {
+        $message = new Message();
+        $message->setScope('iDontKnowScope');
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->never())
+            ->method('sendToRouter')
+        ;
+        $driver
+            ->expects($this->never())
+            ->method('sendToProcessor')
+        ;
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock());
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The message scope "iDontKnowScope" is not supported.');
+        $producer->sendEvent('topic', $message);
+    }
+
+    public function testShouldCallPreSendEventExtensionMethodWhenSendToBus()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_MESSAGE_BUS);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $extension = $this->createMock(ExtensionInterface::class);
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension);
+
+        $extension
+            ->expects($this->at(0))
+            ->method('onPreSendEvent')
+            ->willReturnCallback(function (PreSend $context) use ($message, $producer, $driver) {
+                $this->assertSame($message, $context->getMessage());
+                $this->assertSame($producer, $context->getProducer());
+                $this->assertSame($driver, $context->getDriver());
+                $this->assertSame('topic', $context->getTopic());
+
+                $this->assertEquals($message, $context->getOriginalMessage());
+                $this->assertNotSame($message, $context->getOriginalMessage());
+            });
+
+        $extension
+            ->expects($this->never())
+            ->method('onPreSendCommand')
+        ;
+
+        $producer->sendEvent('topic', $message);
+    }
+
+    public function testShouldCallPreSendEventExtensionMethodWhenSendToApplicationRouter()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_APP);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $extension = $this->createMock(ExtensionInterface::class);
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension);
+
+        $extension
+            ->expects($this->at(0))
+            ->method('onPreSendEvent')
+            ->willReturnCallback(function (PreSend $context) use ($message, $producer, $driver) {
+                $this->assertSame($message, $context->getMessage());
+                $this->assertSame($producer, $context->getProducer());
+                $this->assertSame($driver, $context->getDriver());
+                $this->assertSame('topic', $context->getTopic());
+
+                $this->assertEquals($message, $context->getOriginalMessage());
+                $this->assertNotSame($message, $context->getOriginalMessage());
+            });
+
+        $extension
+            ->expects($this->never())
+            ->method('onPreSendCommand')
+        ;
+
+        $producer->sendEvent('topic', $message);
+    }
+
+    public function testShouldCallPreDriverSendExtensionMethodWhenSendToMessageBus()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_MESSAGE_BUS);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $extension = $this->createMock(ExtensionInterface::class);
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension);
+
+        $extension
+            ->expects($this->at(0))
+            ->method('onDriverPreSend')
+            ->willReturnCallback(function (DriverPreSend $context) use ($message, $producer, $driver) {
+                $this->assertSame($message, $context->getMessage());
+                $this->assertSame($producer, $context->getProducer());
+                $this->assertSame($driver, $context->getDriver());
+                $this->assertSame('topic', $context->getTopic());
+
+                $this->assertTrue($context->isEvent());
+            });
+
+        $producer->sendEvent('topic', $message);
+    }
+
+    public function testShouldCallPreDriverSendExtensionMethodWhenSendToApplicationRouter()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_APP);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $extension = $this->createMock(ExtensionInterface::class);
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension);
+
+        $extension
+            ->expects($this->at(0))
+            ->method('onDriverPreSend')
+            ->willReturnCallback(function (DriverPreSend $context) use ($message, $producer, $driver) {
+                $this->assertSame($message, $context->getMessage());
+                $this->assertSame($producer, $context->getProducer());
+                $this->assertSame($driver, $context->getDriver());
+                $this->assertSame('topic', $context->getTopic());
+
+                $this->assertTrue($context->isEvent());
+            });
+
+        $producer->sendEvent('topic', $message);
+    }
+
+    public function testShouldCallPostSendExtensionMethodWhenSendToMessageBus()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_MESSAGE_BUS);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToRouter')
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $extension = $this->createMock(ExtensionInterface::class);
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension);
+
+        $extension
+            ->expects($this->at(0))
+            ->method('onPostSend')
+            ->willReturnCallback(function (PostSend $context) use ($message, $producer, $driver) {
+                $this->assertSame($message, $context->getMessage());
+                $this->assertSame($producer, $context->getProducer());
+                $this->assertSame($driver, $context->getDriver());
+                $this->assertSame('topic', $context->getTopic());
+
+                $this->assertTrue($context->isEvent());
+            });
+
+        $producer->sendEvent('topic', $message);
+    }
+
+    public function testShouldCallPostSendExtensionMethodWhenSendToApplicationRouter()
+    {
+        $message = new Message();
+        $message->setBody('aBody');
+        $message->setScope(Message::SCOPE_APP);
+
+        $driver = $this->createDriverStub();
+        $driver
+            ->expects($this->once())
+            ->method('sendToProcessor')
+            ->willReturn($this->createDriverSendResult())
+        ;
+
+        $extension = $this->createMock(ExtensionInterface::class);
+
+        $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension);
+
+        $extension
+            ->expects($this->at(0))
+            ->method('onPostSend')
+            ->willReturnCallback(function (PostSend $context) use ($message, $producer, $driver) {
+                $this->assertSame($message, $context->getMessage());
+                $this->assertSame($producer, $context->getProducer());
+                $this->assertSame($driver, $context->getDriver());
+                $this->assertSame('topic', $context->getTopic());
+
+                $this->assertTrue($context->isEvent());
+            });
+
+        $producer->sendEvent('topic', $message);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createRpcFactoryMock(): RpcFactory
+    {
+        return $this->createMock(RpcFactory::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createDriverStub(): DriverInterface
+    {
+        $config = new Config(
+            'a_prefix',
+            '.',
+            'an_app',
+            'a_router_topic',
+            'a_router_queue',
+            'a_default_processor_queue',
+            'a_router_processor_name',
+            [],
+            []
+        );
+
+        $driverMock = $this->createMock(DriverInterface::class);
+        $driverMock
+            ->expects($this->any())
+            ->method('getConfig')
+            ->willReturn($config)
+        ;
+
+        return $driverMock;
+    }
+
+    private function createDriverSendResult(): DriverSendResult
+    {
+        return new DriverSendResult(
+            $this->createMock(Destination::class),
+            $this->createMock(TransportMessage::class)
+        );
+    }
+}
diff --git a/Tests/Client/ProducerTest.php b/Tests/Client/ProducerTest.php
index c9a30fc..23b004a 100644
--- a/Tests/Client/ProducerTest.php
+++ b/Tests/Client/ProducerTest.php
@@ -2,14 +2,9 @@
 
 namespace Enqueue\Tests\Client;
 
-use Enqueue\Client\Config;
 use Enqueue\Client\DriverInterface;
-use Enqueue\Client\ExtensionInterface;
-use Enqueue\Client\Message;
-use Enqueue\Client\MessagePriority;
 use Enqueue\Client\Producer;
 use Enqueue\Client\ProducerInterface;
-use Enqueue\Null\NullQueue;
 use Enqueue\Rpc\RpcFactory;
 use Enqueue\Test\ClassExtensionTrait;
 use PHPUnit\Framework\TestCase;
@@ -23,619 +18,24 @@ public function testShouldImplementProducerInterface()
         self::assertClassImplements(ProducerInterface::class, Producer::class);
     }
 
-    public function testCouldBeConstructedWithDriverAsFirstArgument()
+    public function testShouldBeFinal()
     {
-        new Producer($this->createDriverStub(), $this->createRpcFactory());
-    }
-
-    public function testShouldSendMessageToRouter()
-    {
-        $message = new Message();
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->with(self::identicalTo($message))
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $message);
-
-        $expectedProperties = [
-            'enqueue.topic_name' => 'topic',
-        ];
-
-        self::assertEquals($expectedProperties, $message->getProperties());
-    }
-
-    public function testShouldSendMessageWithNormalPriorityByDefault()
-    {
-        $message = new Message();
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->with(self::identicalTo($message))
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $message);
-
-        self::assertSame(MessagePriority::NORMAL, $message->getPriority());
-    }
-
-    public function testShouldSendMessageWithCustomPriority()
-    {
-        $message = new Message();
-        $message->setPriority(MessagePriority::HIGH);
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->with(self::identicalTo($message))
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $message);
-
-        self::assertSame(MessagePriority::HIGH, $message->getPriority());
-    }
-
-    public function testShouldSendMessageWithGeneratedMessageId()
-    {
-        $message = new Message();
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->with(self::identicalTo($message))
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $message);
-
-        self::assertNotEmpty($message->getMessageId());
-    }
-
-    public function testShouldSendMessageWithCustomMessageId()
-    {
-        $message = new Message();
-        $message->setMessageId('theCustomMessageId');
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->with(self::identicalTo($message))
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $message);
-
-        self::assertSame('theCustomMessageId', $message->getMessageId());
-    }
-
-    public function testShouldSendMessageWithGeneratedTimestamp()
-    {
-        $message = new Message();
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->with(self::identicalTo($message))
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $message);
-
-        self::assertNotEmpty($message->getTimestamp());
-    }
-
-    public function testShouldSendMessageWithCustomTimestamp()
-    {
-        $message = new Message();
-        $message->setTimestamp('theCustomTimestamp');
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->with(self::identicalTo($message))
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $message);
-
-        self::assertSame('theCustomTimestamp', $message->getTimestamp());
-    }
-
-    public function testShouldSendStringAsPlainText()
-    {
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->willReturnCallback(function (Message $message) {
-                self::assertSame('theStringMessage', $message->getBody());
-                self::assertSame('text/plain', $message->getContentType());
-            })
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', 'theStringMessage');
-    }
-
-    public function testShouldSendArrayAsJsonString()
-    {
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->willReturnCallback(function (Message $message) {
-                self::assertSame('{"foo":"fooVal"}', $message->getBody());
-                self::assertSame('application/json', $message->getContentType());
-            })
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', ['foo' => 'fooVal']);
-    }
-
-    public function testShouldConvertMessageArrayBodyJsonString()
-    {
-        $message = new Message();
-        $message->setBody(['foo' => 'fooVal']);
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->willReturnCallback(function (Message $message) {
-                self::assertSame('{"foo":"fooVal"}', $message->getBody());
-                self::assertSame('application/json', $message->getContentType());
-            })
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $message);
-    }
-
-    public function testSendShouldForceScalarsToStringAndSetTextContentType()
-    {
-        $queue = new NullQueue('');
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->willReturnCallback(function (Message $message) {
-                self::assertEquals('text/plain', $message->getContentType());
-
-                self::assertInternalType('string', $message->getBody());
-                self::assertEquals('12345', $message->getBody());
-            })
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent($queue, 12345);
-    }
-
-    public function testSendShouldForceMessageScalarsBodyToStringAndSetTextContentType()
-    {
-        $queue = new NullQueue('');
-
-        $message = new Message();
-        $message->setBody(12345);
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->willReturnCallback(function (Message $message) {
-                self::assertEquals('text/plain', $message->getContentType());
-
-                self::assertInternalType('string', $message->getBody());
-                self::assertEquals('12345', $message->getBody());
-            })
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent($queue, $message);
-    }
-
-    public function testSendShouldForceNullToEmptyStringAndSetTextContentType()
-    {
-        $queue = new NullQueue('');
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->willReturnCallback(function (Message $message) {
-                self::assertEquals('text/plain', $message->getContentType());
-
-                self::assertInternalType('string', $message->getBody());
-                self::assertEquals('', $message->getBody());
-            })
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent($queue, null);
-    }
-
-    public function testSendShouldForceNullBodyToEmptyStringAndSetTextContentType()
-    {
-        $queue = new NullQueue('');
-
-        $message = new Message();
-        $message->setBody(null);
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->willReturnCallback(function (Message $message) {
-                self::assertEquals('text/plain', $message->getContentType());
-
-                self::assertInternalType('string', $message->getBody());
-                self::assertEquals('', $message->getBody());
-            })
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent($queue, $message);
-    }
-
-    public function testShouldThrowExceptionIfBodyIsObjectOnSend()
-    {
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->never())
-            ->method('sendToRouter')
-        ;
-        $driver
-            ->expects($this->never())
-            ->method('sendToProcessor')
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-
-        $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionMessage('The message\'s body must be either null, scalar, array or object (implements \JsonSerializable). Got: stdClass');
-
-        $producer->sendEvent('topic', new \stdClass());
-    }
-
-    public function testShouldThrowExceptionIfBodyIsArrayWithObjectsInsideOnSend()
-    {
-        $queue = new NullQueue('queue');
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->never())
-            ->method('sendToRouter')
-        ;
-        $driver
-            ->expects($this->never())
-            ->method('sendToProcessor')
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The message\'s body must be an array of scalars. Found not scalar in the array: stdClass');
-
-        $producer->sendEvent($queue, ['foo' => new \stdClass()]);
-    }
-
-    public function testShouldThrowExceptionIfBodyIsArrayWithObjectsInSubArraysInsideOnSend()
-    {
-        $queue = new NullQueue('queue');
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->never())
-            ->method('sendToRouter')
-        ;
-        $driver
-            ->expects($this->never())
-            ->method('sendToProcessor')
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The message\'s body must be an array of scalars. Found not scalar in the array: stdClass');
-
-        $producer->sendEvent($queue, ['foo' => ['bar' => new \stdClass()]]);
-    }
-
-    public function testShouldSendJsonSerializableObjectAsJsonStringToMessageBus()
-    {
-        $object = new JsonSerializableObject();
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->willReturnCallback(function (Message $message) {
-                self::assertSame('{"foo":"fooVal"}', $message->getBody());
-                self::assertSame('application/json', $message->getContentType());
-            })
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $object);
-    }
-
-    public function testShouldSendMessageJsonSerializableBodyAsJsonStringToMessageBus()
-    {
-        $object = new JsonSerializableObject();
-
-        $message = new Message();
-        $message->setBody($object);
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-            ->willReturnCallback(function (Message $message) {
-                self::assertSame('{"foo":"fooVal"}', $message->getBody());
-                self::assertSame('application/json', $message->getContentType());
-            })
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $message);
-    }
-
-    public function testThrowIfTryToSendMessageToMessageBusWithProcessorNamePropertySet()
-    {
-        $object = new JsonSerializableObject();
-
-        $message = new Message();
-        $message->setBody($object);
-        $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'aProcessor');
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->never())
-            ->method('sendToRouter')
-        ;
-        $driver
-            ->expects($this->never())
-            ->method('sendToProcessor')
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The enqueue.processor_name property must not be set for messages that are sent to message bus.');
-        $producer->sendEvent('topic', $message);
-    }
-
-    public function testThrowIfTryToSendMessageToMessageBusWithProcessorQueueNamePropertySet()
-    {
-        $object = new JsonSerializableObject();
-
-        $message = new Message();
-        $message->setBody($object);
-        $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aProcessorQueue');
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->never())
-            ->method('sendToRouter')
-        ;
-        $driver
-            ->expects($this->never())
-            ->method('sendToProcessor')
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The enqueue.processor_queue_name property must not be set for messages that are sent to message bus.');
-        $producer->sendEvent('topic', $message);
-    }
-
-    public function testThrowIfNotApplicationJsonContentTypeSetWithJsonSerializableBody()
-    {
-        $object = new JsonSerializableObject();
-
-        $message = new Message();
-        $message->setBody($object);
-        $message->setContentType('foo/bar');
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->never())
-            ->method('sendToRouter')
-        ;
-        $driver
-            ->expects($this->never())
-            ->method('sendToProcessor')
-        ;
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('Content type "application/json" only allowed when body is array');
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $message);
-    }
-
-    public function testShouldSendMessageToApplicationRouter()
-    {
-        $message = new Message();
-        $message->setBody('aBody');
-        $message->setScope(Message::SCOPE_APP);
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->never())
-            ->method('sendToRouter')
-        ;
-        $driver
-            ->expects($this->once())
-            ->method('sendToProcessor')
-            ->willReturnCallback(function (Message $message) {
-                self::assertSame('aBody', $message->getBody());
-                self::assertSame('a_router_processor_name', $message->getProperty(Config::PARAMETER_PROCESSOR_NAME));
-                self::assertSame('a_router_queue', $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME));
-            })
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $message);
-    }
-
-    public function testShouldSendToCustomMessageToApplicationRouter()
-    {
-        $message = new Message();
-        $message->setBody('aBody');
-        $message->setScope(Message::SCOPE_APP);
-        $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'aCustomProcessor');
-        $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aCustomProcessorQueue');
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->never())
-            ->method('sendToRouter')
-        ;
-        $driver
-            ->expects($this->once())
-            ->method('sendToProcessor')
-            ->willReturnCallback(function (Message $message) {
-                self::assertSame('aBody', $message->getBody());
-                self::assertSame('aCustomProcessor', $message->getProperty(Config::PARAMETER_PROCESSOR_NAME));
-                self::assertSame('aCustomProcessorQueue', $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME));
-            })
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-        $producer->sendEvent('topic', $message);
-    }
-
-    public function testThrowIfUnSupportedScopeGivenOnSend()
-    {
-        $message = new Message();
-        $message->setScope('iDontKnowScope');
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->never())
-            ->method('sendToRouter')
-        ;
-        $driver
-            ->expects($this->never())
-            ->method('sendToProcessor')
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory());
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The message scope "iDontKnowScope" is not supported.');
-        $producer->sendEvent('topic', $message);
-    }
-
-    public function testShouldCallPreSendPostSendExtensionMethodsWhenSendToRouter()
-    {
-        $message = new Message();
-        $message->setBody('aBody');
-        $message->setScope(Message::SCOPE_MESSAGE_BUS);
-
-        $extension = $this->createMock(ExtensionInterface::class);
-        $extension
-            ->expects($this->at(0))
-            ->method('onPreSend')
-            ->with($this->identicalTo('topic'), $this->identicalTo($message))
-        ;
-        $extension
-            ->expects($this->at(1))
-            ->method('onPostSend')
-            ->with($this->identicalTo('topic'), $this->identicalTo($message))
-        ;
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToRouter')
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory(), $extension);
-        $producer->sendEvent('topic', $message);
-    }
-
-    public function testShouldCallPreSendPostSendExtensionMethodsWhenSendToProcessor()
-    {
-        $message = new Message();
-        $message->setBody('aBody');
-        $message->setScope(Message::SCOPE_APP);
-
-        $extension = $this->createMock(ExtensionInterface::class);
-        $extension
-            ->expects($this->at(0))
-            ->method('onPreSend')
-            ->with($this->identicalTo('topic'), $this->identicalTo($message))
-        ;
-        $extension
-            ->expects($this->at(1))
-            ->method('onPostSend')
-            ->with($this->identicalTo('topic'), $this->identicalTo($message))
-        ;
-
-        $driver = $this->createDriverStub();
-        $driver
-            ->expects($this->once())
-            ->method('sendToProcessor')
-        ;
-
-        $producer = new Producer($driver, $this->createRpcFactory(), $extension);
-        $producer->sendEvent('topic', $message);
+        self::assertClassFinal(Producer::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|RpcFactory
+     * @return \PHPUnit\Framework\MockObject\MockObject
      */
-    private function createRpcFactory()
+    private function createRpcFactoryMock(): RpcFactory
     {
         return $this->createMock(RpcFactory::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface
+     * @return \PHPUnit\Framework\MockObject\MockObject
      */
-    private function createDriverStub()
-    {
-        $config = new Config(
-            'a_prefix',
-            'an_app',
-            'a_router_topic',
-            'a_router_queue',
-            'a_default_processor_queue',
-            'a_router_processor_name'
-        );
-
-        $driverMock = $this->createMock(DriverInterface::class);
-        $driverMock
-            ->expects($this->any())
-            ->method('getConfig')
-            ->willReturn($config)
-        ;
-
-        return $driverMock;
-    }
-}
-
-class JsonSerializableObject implements \JsonSerializable
-{
-    public function jsonSerialize()
+    private function createDriverMock(): DriverInterface
     {
-        return ['foo' => 'fooVal'];
+        return $this->createMock(DriverInterface::class);
     }
 }
diff --git a/Tests/Client/ResourcesTest.php b/Tests/Client/ResourcesTest.php
new file mode 100644
index 0000000..e79fb9d
--- /dev/null
+++ b/Tests/Client/ResourcesTest.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace Enqueue\Tests\Client;
+
+use Enqueue\Client\Driver\AmqpDriver;
+use Enqueue\Client\Driver\RabbitMqDriver;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Resources;
+use PHPUnit\Framework\TestCase;
+
+class ResourcesTest extends TestCase
+{
+    public function testShouldBeFinal()
+    {
+        $rc = new \ReflectionClass(Resources::class);
+
+        $this->assertTrue($rc->isFinal());
+    }
+
+    public function testShouldConstructorBePrivate()
+    {
+        $rc = new \ReflectionClass(Resources::class);
+
+        $this->assertTrue($rc->getConstructor()->isPrivate());
+    }
+
+    public function testShouldGetAvailableDriverInExpectedFormat()
+    {
+        $availableDrivers = Resources::getAvailableDrivers();
+
+        self::assertIsArray($availableDrivers);
+        $this->assertGreaterThan(0, count($availableDrivers));
+
+        $driverInfo = $availableDrivers[0];
+
+        $this->assertArrayHasKey('driverClass', $driverInfo);
+        $this->assertSame(AmqpDriver::class, $driverInfo['driverClass']);
+
+        $this->assertArrayHasKey('schemes', $driverInfo);
+        $this->assertSame(['amqp', 'amqps'], $driverInfo['schemes']);
+
+        $this->assertArrayHasKey('requiredSchemeExtensions', $driverInfo);
+        $this->assertSame([], $driverInfo['requiredSchemeExtensions']);
+
+        $this->assertArrayHasKey('packages', $driverInfo);
+        $this->assertSame(['enqueue/enqueue', 'enqueue/amqp-bunny'], $driverInfo['packages']);
+    }
+
+    public function testShouldGetAvailableDriverWithRequiredExtensionInExpectedFormat()
+    {
+        $availableDrivers = Resources::getAvailableDrivers();
+
+        self::assertIsArray($availableDrivers);
+        $this->assertGreaterThan(0, count($availableDrivers));
+
+        $driverInfo = $availableDrivers[1];
+
+        $this->assertArrayHasKey('driverClass', $driverInfo);
+        $this->assertSame(RabbitMqDriver::class, $driverInfo['driverClass']);
+
+        $this->assertArrayHasKey('schemes', $driverInfo);
+        $this->assertSame(['amqp', 'amqps'], $driverInfo['schemes']);
+
+        $this->assertArrayHasKey('requiredSchemeExtensions', $driverInfo);
+        $this->assertSame(['rabbitmq'], $driverInfo['requiredSchemeExtensions']);
+
+        $this->assertArrayHasKey('packages', $driverInfo);
+        $this->assertSame(['enqueue/enqueue', 'enqueue/amqp-bunny'], $driverInfo['packages']);
+    }
+
+    public function testShouldGetKnownDriversInExpectedFormat()
+    {
+        $knownDrivers = Resources::getAvailableDrivers();
+
+        self::assertIsArray($knownDrivers);
+        $this->assertGreaterThan(0, count($knownDrivers));
+
+        $driverInfo = $knownDrivers[0];
+
+        $this->assertArrayHasKey('driverClass', $driverInfo);
+        $this->assertSame(AmqpDriver::class, $driverInfo['driverClass']);
+
+        $this->assertArrayHasKey('schemes', $driverInfo);
+        $this->assertSame(['amqp', 'amqps'], $driverInfo['schemes']);
+
+        $this->assertArrayHasKey('requiredSchemeExtensions', $driverInfo);
+        $this->assertSame([], $driverInfo['requiredSchemeExtensions']);
+
+        $this->assertArrayHasKey('packages', $driverInfo);
+        $this->assertSame(['enqueue/enqueue', 'enqueue/amqp-bunny'], $driverInfo['packages']);
+    }
+
+    public function testThrowsIfDriverClassNotImplementDriverFactoryInterfaceOnAddDriver()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The driver class "stdClass" must implement "Enqueue\Client\DriverInterface" interface.');
+
+        Resources::addDriver(\stdClass::class, [], [], ['foo']);
+    }
+
+    public function testThrowsIfNoSchemesProvidedOnAddDriver()
+    {
+        $driverClass = $this->getMockClass(DriverInterface::class);
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Schemes could not be empty.');
+
+        Resources::addDriver($driverClass, [], [], ['foo']);
+    }
+
+    public function testThrowsIfNoPackageProvidedOnAddDriver()
+    {
+        $driverClass = $this->getMockClass(DriverInterface::class);
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Packages could not be empty.');
+
+        Resources::addDriver($driverClass, ['foo'], [], []);
+    }
+
+    public function testShouldAllowRegisterDriverThatIsNotInstalled()
+    {
+        Resources::addDriver('theDriverClass', ['foo'], ['barExtension'], ['foo']);
+
+        $availableDrivers = Resources::getKnownDrivers();
+
+        $driverInfo = end($availableDrivers);
+
+        $this->assertSame('theDriverClass', $driverInfo['driverClass']);
+    }
+
+    public function testShouldAllowGetPreviouslyRegisteredDriver()
+    {
+        $driverClass = $this->getMockClass(DriverInterface::class);
+
+        Resources::addDriver(
+            $driverClass,
+            ['fooscheme', 'barscheme'],
+            ['fooextension', 'barextension'],
+            ['foo/bar']
+        );
+
+        $availableDrivers = Resources::getAvailableDrivers();
+
+        $driverInfo = end($availableDrivers);
+
+        $this->assertArrayHasKey('driverClass', $driverInfo);
+        $this->assertSame($driverClass, $driverInfo['driverClass']);
+
+        $this->assertArrayHasKey('schemes', $driverInfo);
+        $this->assertSame(['fooscheme', 'barscheme'], $driverInfo['schemes']);
+
+        $this->assertArrayHasKey('requiredSchemeExtensions', $driverInfo);
+        $this->assertSame(['fooextension', 'barextension'], $driverInfo['requiredSchemeExtensions']);
+
+        $this->assertArrayHasKey('packages', $driverInfo);
+        $this->assertSame(['foo/bar'], $driverInfo['packages']);
+    }
+}
diff --git a/Tests/Client/RouterProcessorTest.php b/Tests/Client/RouterProcessorTest.php
index e362c1e..7d29711 100644
--- a/Tests/Client/RouterProcessorTest.php
+++ b/Tests/Client/RouterProcessorTest.php
@@ -4,203 +4,218 @@
 
 use Enqueue\Client\Config;
 use Enqueue\Client\DriverInterface;
+use Enqueue\Client\DriverSendResult;
 use Enqueue\Client\Message;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
 use Enqueue\Client\RouterProcessor;
 use Enqueue\Consumption\Result;
 use Enqueue\Null\NullContext;
 use Enqueue\Null\NullMessage;
+use Enqueue\Test\ClassExtensionTrait;
+use Enqueue\Test\ReadAttributeTrait;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as TransportMessage;
+use Interop\Queue\Processor;
 use PHPUnit\Framework\TestCase;
 
 class RouterProcessorTest extends TestCase
 {
-    public function testCouldBeConstructedWithDriverAsFirstArgument()
+    use ClassExtensionTrait;
+    use ReadAttributeTrait;
+
+    public function testShouldImplementProcessorInterface()
+    {
+        $this->assertClassImplements(Processor::class, RouterProcessor::class);
+    }
+
+    public function testShouldBeFinal()
     {
-        new RouterProcessor($this->createDriverMock());
+        $this->assertClassFinal(RouterProcessor::class);
     }
 
-    public function testCouldBeConstructedWithSessionAndRoutes()
+    public function testCouldBeConstructedWithDriver()
     {
-        $routes = [
-            'aTopicName' => [['aProcessorName', 'aQueueName']],
-            'anotherTopicName' => [['aProcessorName', 'aQueueName']],
-        ];
+        $driver = $this->createDriverStub();
 
-        $router = new RouterProcessor($this->createDriverMock(), $routes);
+        $processor = new RouterProcessor($driver);
 
-        $this->assertAttributeEquals($routes, 'eventRoutes', $router);
+        $this->assertAttributeSame($driver, 'driver', $processor);
     }
 
-    public function testShouldRejectIfTopicNameParameterIsNotSet()
+    public function testShouldRejectIfTopicNotSet()
     {
-        $router = new RouterProcessor($this->createDriverMock());
+        $router = new RouterProcessor($this->createDriverStub());
 
         $result = $router->process(new NullMessage(), new NullContext());
 
-        $this->assertInstanceOf(Result::class, $result);
         $this->assertEquals(Result::REJECT, $result->getStatus());
-        $this->assertEquals('Got message without required parameter: "enqueue.topic_name"', $result->getReason());
+        $this->assertEquals('Topic property "enqueue.topic" is required but not set or empty.', $result->getReason());
+    }
+
+    public function testShouldRejectIfCommandSet()
+    {
+        $router = new RouterProcessor($this->createDriverStub());
+
+        $message = new NullMessage();
+        $message->setProperty(Config::COMMAND, 'aCommand');
+
+        $result = $router->process($message, new NullContext());
+
+        $this->assertEquals(Result::REJECT, $result->getStatus());
+        $this->assertEquals('Unexpected command "aCommand" got. Command must not go to the router.', $result->getReason());
     }
 
-    public function testShouldRouteOriginalMessageToEventRecipient()
+    public function testShouldRouteOriginalMessageToAllRecipients()
     {
         $message = new NullMessage();
         $message->setBody('theBody');
         $message->setHeaders(['aHeader' => 'aHeaderVal']);
-        $message->setProperties(['aProp' => 'aPropVal', Config::PARAMETER_TOPIC_NAME => 'theTopicName']);
+        $message->setProperties(['aProp' => 'aPropVal', Config::TOPIC => 'theTopicName']);
 
-        $clientMessage = new Message();
+        /** @var Message[] $routedMessages */
+        $routedMessages = new \ArrayObject();
 
-        $routedMessage = null;
+        $routeCollection = new RouteCollection([
+            new Route('theTopicName', Route::TOPIC, 'aFooProcessor'),
+            new Route('theTopicName', Route::TOPIC, 'aBarProcessor'),
+            new Route('theTopicName', Route::TOPIC, 'aBazProcessor'),
+        ]);
 
-        $driver = $this->createDriverMock();
+        $driver = $this->createDriverStub($routeCollection);
         $driver
-            ->expects($this->once())
+            ->expects($this->exactly(3))
             ->method('sendToProcessor')
-            ->with($this->identicalTo($clientMessage))
+            ->willReturnCallback(function (Message $message) use ($routedMessages) {
+                $routedMessages->append($message);
+
+                return $this->createDriverSendResult();
+            })
         ;
         $driver
-            ->expects($this->once())
+            ->expects($this->exactly(3))
             ->method('createClientMessage')
-            ->willReturnCallback(function (NullMessage $message) use (&$routedMessage, $clientMessage) {
-                $routedMessage = $message;
-
-                return $clientMessage;
+            ->willReturnCallback(function (NullMessage $message) {
+                return new Message($message->getBody(), $message->getProperties(), $message->getHeaders());
             })
         ;
 
-        $routes = [
-            'theTopicName' => [['aFooProcessor', 'aQueueName']],
-        ];
+        $processor = new RouterProcessor($driver);
 
-        $router = new RouterProcessor($driver, $routes);
+        $result = $processor->process($message, new NullContext());
 
-        $result = $router->process($message, new NullContext());
+        $this->assertEquals(Result::ACK, $result->getStatus());
+        $this->assertEquals('Routed to "3" event subscribers', $result->getReason());
+
+        $this->assertContainsOnly(Message::class, $routedMessages);
+        $this->assertCount(3, $routedMessages);
 
-        $this->assertEquals(Result::ACK, $result);
-        $this->assertEquals([
-            'aProp' => 'aPropVal',
-            'enqueue.topic_name' => 'theTopicName',
-            'enqueue.processor_name' => 'aFooProcessor',
-            'enqueue.processor_queue_name' => 'aQueueName',
-        ], $routedMessage->getProperties());
+        $this->assertSame('aFooProcessor', $routedMessages[0]->getProperty(Config::PROCESSOR));
+        $this->assertSame('aBarProcessor', $routedMessages[1]->getProperty(Config::PROCESSOR));
+        $this->assertSame('aBazProcessor', $routedMessages[2]->getProperty(Config::PROCESSOR));
     }
 
-    public function testShouldRouteOriginalMessageToCommandRecipient()
+    public function testShouldDoNothingIfNoRoutes()
     {
         $message = new NullMessage();
         $message->setBody('theBody');
         $message->setHeaders(['aHeader' => 'aHeaderVal']);
-        $message->setProperties([
-            'aProp' => 'aPropVal',
-            Config::PARAMETER_TOPIC_NAME => Config::COMMAND_TOPIC,
-            Config::PARAMETER_COMMAND_NAME => 'theCommandName',
-        ]);
+        $message->setProperties(['aProp' => 'aPropVal', Config::TOPIC => 'theTopicName']);
 
-        $clientMessage = new Message();
+        /** @var Message[] $routedMessages */
+        $routedMessages = new \ArrayObject();
 
-        $routedMessage = null;
+        $routeCollection = new RouteCollection([]);
 
-        $driver = $this->createDriverMock();
+        $driver = $this->createDriverStub($routeCollection);
         $driver
-            ->expects($this->once())
+            ->expects($this->never())
             ->method('sendToProcessor')
-            ->with($this->identicalTo($clientMessage))
+            ->willReturnCallback(function (Message $message) use ($routedMessages) {
+                $routedMessages->append($message);
+            })
         ;
         $driver
-            ->expects($this->once())
+            ->expects($this->never())
             ->method('createClientMessage')
-            ->willReturnCallback(function (NullMessage $message) use (&$routedMessage, $clientMessage) {
-                $routedMessage = $message;
-
-                return $clientMessage;
+            ->willReturnCallback(function (NullMessage $message) {
+                return new Message($message->getBody(), $message->getProperties(), $message->getHeaders());
             })
         ;
 
-        $routes = [
-            'theCommandName' => 'aQueueName',
-        ];
+        $processor = new RouterProcessor($driver);
 
-        $router = new RouterProcessor($driver, [], $routes);
+        $result = $processor->process($message, new NullContext());
 
-        $result = $router->process($message, new NullContext());
+        $this->assertEquals(Result::ACK, $result->getStatus());
+        $this->assertEquals('Routed to "0" event subscribers', $result->getReason());
 
-        $this->assertEquals(Result::ACK, $result);
-        $this->assertEquals([
-            'aProp' => 'aPropVal',
-            'enqueue.topic_name' => Config::COMMAND_TOPIC,
-            'enqueue.processor_name' => 'theCommandName',
-            'enqueue.command_name' => 'theCommandName',
-            'enqueue.processor_queue_name' => 'aQueueName',
-        ], $routedMessage->getProperties());
+        $this->assertCount(0, $routedMessages);
     }
 
-    public function testShouldRejectCommandMessageIfCommandNamePropertyMissing()
+    public function testShouldDoNotModifyOriginalMessage()
     {
         $message = new NullMessage();
         $message->setBody('theBody');
         $message->setHeaders(['aHeader' => 'aHeaderVal']);
-        $message->setProperties([
-            'aProp' => 'aPropVal',
-            Config::PARAMETER_TOPIC_NAME => Config::COMMAND_TOPIC,
+        $message->setProperties(['aProp' => 'aPropVal', Config::TOPIC => 'theTopicName']);
+
+        /** @var Message[] $routedMessages */
+        $routedMessages = new \ArrayObject();
+
+        $routeCollection = new RouteCollection([
+            new Route('theTopicName', Route::TOPIC, 'aFooProcessor'),
+            new Route('theTopicName', Route::TOPIC, 'aBarProcessor'),
         ]);
 
-        $driver = $this->createDriverMock();
+        $driver = $this->createDriverStub($routeCollection);
         $driver
-            ->expects($this->never())
+            ->expects($this->atLeastOnce())
             ->method('sendToProcessor')
-        ;
+            ->willReturnCallback(function (Message $message) use ($routedMessages) {
+                $routedMessages->append($message);
+
+                return $this->createDriverSendResult();
+            });
         $driver
-            ->expects($this->never())
+            ->expects($this->atLeastOnce())
             ->method('createClientMessage')
-        ;
-
-        $routes = [
-            'theCommandName' => 'aQueueName',
-        ];
-
-        $router = new RouterProcessor($driver, [], $routes);
-
-        $result = $router->process($message, new NullContext());
-
-        $this->assertInstanceOf(Result::class, $result);
-        $this->assertEquals(Result::REJECT, $result->getStatus());
-        $this->assertEquals('Got message without required parameter: "enqueue.command_name"', $result->getReason());
-    }
-
-    public function testShouldAddEventRoute()
-    {
-        $router = new RouterProcessor($this->createDriverMock(), []);
+            ->willReturnCallback(function (NullMessage $message) {
+                return new Message($message->getBody(), $message->getProperties(), $message->getHeaders());
+            });
 
-        $this->assertAttributeSame([], 'eventRoutes', $router);
+        $processor = new RouterProcessor($driver);
 
-        $router->add('theTopicName', 'theQueueName', 'aProcessorName');
+        $result = $processor->process($message, new NullContext());
 
-        $this->assertAttributeSame([
-            'theTopicName' => [
-                ['aProcessorName', 'theQueueName'],
-            ],
-        ], 'eventRoutes', $router);
+        // guard
+        $this->assertEquals(Result::ACK, $result->getStatus());
 
-        $this->assertAttributeSame([], 'commandRoutes', $router);
+        $this->assertSame('theBody', $message->getBody());
+        $this->assertSame(['aProp' => 'aPropVal', Config::TOPIC => 'theTopicName'], $message->getProperties());
+        $this->assertSame(['aHeader' => 'aHeaderVal'], $message->getHeaders());
     }
 
-    public function testShouldAddCommandRoute()
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface
+     */
+    private function createDriverStub(?RouteCollection $routeCollection = null): DriverInterface
     {
-        $router = new RouterProcessor($this->createDriverMock(), []);
-
-        $this->assertAttributeSame([], 'eventRoutes', $router);
-
-        $router->add(Config::COMMAND_TOPIC, 'theQueueName', 'aProcessorName');
+        $driver = $this->createMock(DriverInterface::class);
+        $driver
+            ->expects($this->any())
+            ->method('getRouteCollection')
+            ->willReturn($routeCollection ?? new RouteCollection([]))
+        ;
 
-        $this->assertAttributeSame(['aProcessorName' => 'theQueueName'], 'commandRoutes', $router);
-        $this->assertAttributeSame([], 'eventRoutes', $router);
+        return $driver;
     }
 
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface
-     */
-    protected function createDriverMock()
+    private function createDriverSendResult(): DriverSendResult
     {
-        return $this->createMock(DriverInterface::class);
+        return new DriverSendResult(
+            $this->createMock(Destination::class),
+            $this->createMock(TransportMessage::class)
+        );
     }
 }
diff --git a/Tests/Client/SpoolProducerTest.php b/Tests/Client/SpoolProducerTest.php
index 8c00ded..014fe49 100644
--- a/Tests/Client/SpoolProducerTest.php
+++ b/Tests/Client/SpoolProducerTest.php
@@ -18,11 +18,6 @@ public function testShouldImplementProducerInterface()
         self::assertClassImplements(ProducerInterface::class, SpoolProducer::class);
     }
 
-    public function testCouldBeConstructedWithRealProducer()
-    {
-        new SpoolProducer($this->createProducerMock());
-    }
-
     public function testShouldQueueEventMessageOnSend()
     {
         $message = new Message();
@@ -154,7 +149,7 @@ public function testShouldSendImmediatelyCommandMessageWithNeedReplyTrue()
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|ProducerInterface
+     * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface
      */
     protected function createProducerMock()
     {
diff --git a/Tests/Client/TraceableProducerTest.php b/Tests/Client/TraceableProducerTest.php
index 42c9c8b..b0df066 100644
--- a/Tests/Client/TraceableProducerTest.php
+++ b/Tests/Client/TraceableProducerTest.php
@@ -2,7 +2,7 @@
 
 namespace Enqueue\Tests\Client;
 
-use Enqueue\Client\Config;
+use DMS\PHPUnitExtensions\ArraySubset\Assert;
 use Enqueue\Client\Message;
 use Enqueue\Client\ProducerInterface;
 use Enqueue\Client\TraceableProducer;
@@ -18,11 +18,6 @@ public function testShouldImplementProducerInterface()
         $this->assertClassImplements(ProducerInterface::class, TraceableProducer::class);
     }
 
-    public function testCouldBeConstructedWithInternalMessageProducer()
-    {
-        new TraceableProducer($this->createProducerMock());
-    }
-
     public function testShouldPassAllArgumentsToInternalEventMessageProducerSendMethod()
     {
         $topic = 'theTopic';
@@ -46,7 +41,7 @@ public function testShouldCollectInfoIfStringGivenAsEventMessage()
 
         $producer->sendEvent('aFooTopic', 'aFooBody');
 
-        $this->assertSame([
+        Assert::assertArraySubset([
             [
                 'topic' => 'aFooTopic',
                 'command' => null,
@@ -61,6 +56,8 @@ public function testShouldCollectInfoIfStringGivenAsEventMessage()
                 'messageId' => null,
             ],
         ], $producer->getTraces());
+
+        $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]);
     }
 
     public function testShouldCollectInfoIfArrayGivenAsEventMessage()
@@ -69,7 +66,7 @@ public function testShouldCollectInfoIfArrayGivenAsEventMessage()
 
         $producer->sendEvent('aFooTopic', ['foo' => 'fooVal', 'bar' => 'barVal']);
 
-        $this->assertSame([
+        Assert::assertArraySubset([
             [
                 'topic' => 'aFooTopic',
                 'command' => null,
@@ -84,6 +81,8 @@ public function testShouldCollectInfoIfArrayGivenAsEventMessage()
                 'messageId' => null,
             ],
         ], $producer->getTraces());
+
+        $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]);
     }
 
     public function testShouldCollectInfoIfEventMessageObjectGivenAsMessage()
@@ -103,7 +102,7 @@ public function testShouldCollectInfoIfEventMessageObjectGivenAsMessage()
 
         $producer->sendEvent('aFooTopic', $message);
 
-        $this->assertSame([
+        Assert::assertArraySubset([
             [
                 'topic' => 'aFooTopic',
                 'command' => null,
@@ -118,6 +117,8 @@ public function testShouldCollectInfoIfEventMessageObjectGivenAsMessage()
                 'messageId' => 'theMessageId',
             ],
         ], $producer->getTraces());
+
+        $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]);
     }
 
     public function testShouldNotStoreAnythingIfInternalEventMessageProducerThrowsException()
@@ -163,9 +164,9 @@ public function testShouldCollectInfoIfStringGivenAsCommandMessage()
 
         $producer->sendCommand('aFooCommand', 'aFooBody');
 
-        $this->assertSame([
+        Assert::assertArraySubset([
             [
-                'topic' => Config::COMMAND_TOPIC,
+                'topic' => null,
                 'command' => 'aFooCommand',
                 'body' => 'aFooBody',
                 'headers' => [],
@@ -178,6 +179,8 @@ public function testShouldCollectInfoIfStringGivenAsCommandMessage()
                 'messageId' => null,
             ],
         ], $producer->getTraces());
+
+        $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]);
     }
 
     public function testShouldCollectInfoIfArrayGivenAsCommandMessage()
@@ -186,9 +189,9 @@ public function testShouldCollectInfoIfArrayGivenAsCommandMessage()
 
         $producer->sendCommand('aFooCommand', ['foo' => 'fooVal', 'bar' => 'barVal']);
 
-        $this->assertSame([
+        Assert::assertArraySubset([
             [
-                'topic' => Config::COMMAND_TOPIC,
+                'topic' => null,
                 'command' => 'aFooCommand',
                 'body' => ['foo' => 'fooVal', 'bar' => 'barVal'],
                 'headers' => [],
@@ -201,6 +204,8 @@ public function testShouldCollectInfoIfArrayGivenAsCommandMessage()
                 'messageId' => null,
             ],
         ], $producer->getTraces());
+
+        $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]);
     }
 
     public function testShouldCollectInfoIfCommandMessageObjectGivenAsMessage()
@@ -220,9 +225,9 @@ public function testShouldCollectInfoIfCommandMessageObjectGivenAsMessage()
 
         $producer->sendCommand('aFooCommand', $message);
 
-        $this->assertSame([
+        Assert::assertArraySubset([
             [
-                'topic' => Config::COMMAND_TOPIC,
+                'topic' => null,
                 'command' => 'aFooCommand',
                 'body' => ['foo' => 'fooVal', 'bar' => 'barVal'],
                 'headers' => ['fooHeader' => 'fooVal'],
@@ -235,6 +240,8 @@ public function testShouldCollectInfoIfCommandMessageObjectGivenAsMessage()
                 'messageId' => 'theMessageId',
             ],
         ], $producer->getTraces());
+
+        $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]);
     }
 
     public function testShouldNotStoreAnythingIfInternalCommandMessageProducerThrowsException()
@@ -264,9 +271,9 @@ public function testShouldAllowGetInfoSentToSameTopic()
         $producer->sendEvent('aFooTopic', 'aFooBody');
         $producer->sendEvent('aFooTopic', 'aFooBody');
 
-        $this->assertArraySubset([
-                ['topic' => 'aFooTopic', 'body' => 'aFooBody'],
-                ['topic' => 'aFooTopic', 'body' => 'aFooBody'],
+        Assert::assertArraySubset([
+            ['topic' => 'aFooTopic', 'body' => 'aFooBody'],
+            ['topic' => 'aFooTopic', 'body' => 'aFooBody'],
         ], $producer->getTraces());
     }
 
@@ -277,7 +284,7 @@ public function testShouldAllowGetInfoSentToDifferentTopics()
         $producer->sendEvent('aFooTopic', 'aFooBody');
         $producer->sendEvent('aBarTopic', 'aBarBody');
 
-        $this->assertArraySubset([
+        Assert::assertArraySubset([
             ['topic' => 'aFooTopic', 'body' => 'aFooBody'],
             ['topic' => 'aBarTopic', 'body' => 'aBarBody'],
         ], $producer->getTraces());
@@ -290,11 +297,11 @@ public function testShouldAllowGetInfoSentToSpecialTopic()
         $producer->sendEvent('aFooTopic', 'aFooBody');
         $producer->sendEvent('aBarTopic', 'aBarBody');
 
-        $this->assertArraySubset([
+        Assert::assertArraySubset([
             ['topic' => 'aFooTopic', 'body' => 'aFooBody'],
         ], $producer->getTopicTraces('aFooTopic'));
 
-        $this->assertArraySubset([
+        Assert::assertArraySubset([
             ['topic' => 'aBarTopic', 'body' => 'aBarBody'],
         ], $producer->getTopicTraces('aBarTopic'));
     }
@@ -306,7 +313,7 @@ public function testShouldAllowGetInfoSentToSameCommand()
         $producer->sendCommand('aFooCommand', 'aFooBody');
         $producer->sendCommand('aFooCommand', 'aFooBody');
 
-        $this->assertArraySubset([
+        Assert::assertArraySubset([
             ['command' => 'aFooCommand', 'body' => 'aFooBody'],
             ['command' => 'aFooCommand', 'body' => 'aFooBody'],
         ], $producer->getTraces());
@@ -319,7 +326,7 @@ public function testShouldAllowGetInfoSentToDifferentCommands()
         $producer->sendCommand('aFooCommand', 'aFooBody');
         $producer->sendCommand('aBarCommand', 'aBarBody');
 
-        $this->assertArraySubset([
+        Assert::assertArraySubset([
             ['command' => 'aFooCommand', 'body' => 'aFooBody'],
             ['command' => 'aBarCommand', 'body' => 'aBarBody'],
         ], $producer->getTraces());
@@ -332,11 +339,11 @@ public function testShouldAllowGetInfoSentToSpecialCommand()
         $producer->sendCommand('aFooCommand', 'aFooBody');
         $producer->sendCommand('aBarCommand', 'aBarBody');
 
-        $this->assertArraySubset([
+        Assert::assertArraySubset([
             ['command' => 'aFooCommand', 'body' => 'aFooBody'],
         ], $producer->getCommandTraces('aFooCommand'));
 
-        $this->assertArraySubset([
+        Assert::assertArraySubset([
             ['command' => 'aBarCommand', 'body' => 'aBarBody'],
         ], $producer->getCommandTraces('aBarCommand'));
     }
@@ -347,7 +354,7 @@ public function testShouldAllowClearStoredTraces()
 
         $producer->sendEvent('aFooTopic', 'aFooBody');
 
-        //guard
+        // guard
         $this->assertNotEmpty($producer->getTraces());
 
         $producer->clearTraces();
@@ -355,7 +362,7 @@ public function testShouldAllowClearStoredTraces()
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|ProducerInterface
+     * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface
      */
     protected function createProducerMock()
     {
diff --git a/Tests/ConnectionFactoryFactoryTest.php b/Tests/ConnectionFactoryFactoryTest.php
new file mode 100644
index 0000000..b6b5b4d
--- /dev/null
+++ b/Tests/ConnectionFactoryFactoryTest.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace Enqueue\Tests;
+
+use Enqueue\AmqpBunny\AmqpConnectionFactory as AmqpBunnyConnectionFactory;
+use Enqueue\AmqpExt\AmqpConnectionFactory as AmqpExtConnectionFactory;
+use Enqueue\AmqpLib\AmqpConnectionFactory as AmqpLibConnectionFactory;
+use Enqueue\ConnectionFactoryFactory;
+use Enqueue\ConnectionFactoryFactoryInterface;
+use Enqueue\Dbal\DbalConnectionFactory;
+use Enqueue\Fs\FsConnectionFactory;
+use Enqueue\Gearman\GearmanConnectionFactory;
+use Enqueue\Gps\GpsConnectionFactory;
+use Enqueue\Mongodb\MongodbConnectionFactory;
+use Enqueue\Null\NullConnectionFactory;
+use Enqueue\Pheanstalk\PheanstalkConnectionFactory;
+use Enqueue\RdKafka\RdKafkaConnectionFactory;
+use Enqueue\Redis\RedisConnectionFactory;
+use Enqueue\Resources;
+use Enqueue\Sqs\SqsConnectionFactory;
+use Enqueue\Stomp\StompConnectionFactory;
+use PHPUnit\Framework\TestCase;
+
+class ConnectionFactoryFactoryTest extends TestCase
+{
+    public function testShouldImplementConnectionFactoryFactoryInterface()
+    {
+        $rc = new \ReflectionClass(ConnectionFactoryFactory::class);
+
+        $this->assertTrue($rc->implementsInterface(ConnectionFactoryFactoryInterface::class));
+    }
+
+    public function testShouldBeFinal()
+    {
+        $rc = new \ReflectionClass(ConnectionFactoryFactory::class);
+
+        $this->assertTrue($rc->isFinal());
+    }
+
+    /**
+     * @doesNotPerformAssertions
+     */
+    public function testShouldAcceptStringDSN()
+    {
+        $factory = new ConnectionFactoryFactory();
+
+        $factory->create('null:');
+    }
+
+    /**
+     * @doesNotPerformAssertions
+     */
+    public function testShouldAcceptArrayWithDsnKey()
+    {
+        $factory = new ConnectionFactoryFactory();
+
+        $factory->create(['dsn' => 'null:']);
+    }
+
+    public function testThrowIfInvalidConfigGiven()
+    {
+        $factory = new ConnectionFactoryFactory();
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The config must be either array or DSN string.');
+        $factory->create(new \stdClass());
+    }
+
+    public function testThrowIfArrayConfigMissDsnKeyInvalidConfigGiven()
+    {
+        $factory = new ConnectionFactoryFactory();
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The config must be either array or DSN string.');
+        $factory->create(new \stdClass());
+    }
+
+    public function testThrowIfPackageThatSupportSchemeNotInstalled()
+    {
+        $scheme = 'scheme5b7aa7d7cd213';
+        $class = 'ConnectionClass5b7aa7d7cd213';
+
+        Resources::addConnection($class, [$scheme], [], 'thePackage');
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('To use given scheme "scheme5b7aa7d7cd213" a package has to be installed. Run "composer req thePackage" to add it.');
+        (new ConnectionFactoryFactory())->create($scheme.'://foo');
+    }
+
+    public function testThrowIfSchemeIsNotKnown()
+    {
+        $scheme = 'scheme5b7aa862e70a5';
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('A given scheme "scheme5b7aa862e70a5" is not supported. Maybe it is a custom connection, make sure you registered it with "Enqueue\Resources::addConnection".');
+        (new ConnectionFactoryFactory())->create($scheme.'://foo');
+    }
+
+    public function testThrowIfDsnInvalid()
+    {
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The DSN is invalid. It does not have scheme separator ":".');
+
+        (new ConnectionFactoryFactory())->create('invalid-scheme');
+    }
+
+    /**
+     * @dataProvider provideDSN
+     */
+    public function testReturnsExpectedFactories(string $dsn, string $expectedFactoryClass)
+    {
+        $connectionFactory = (new ConnectionFactoryFactory())->create($dsn);
+
+        $this->assertInstanceOf($expectedFactoryClass, $connectionFactory);
+    }
+
+    public static function provideDSN()
+    {
+        yield ['null:', NullConnectionFactory::class];
+
+        yield ['amqp:', AmqpBunnyConnectionFactory::class];
+
+        yield ['amqp+bunny:', AmqpBunnyConnectionFactory::class];
+
+        yield ['amqp+lib:', AmqpLibConnectionFactory::class];
+
+        yield ['amqp+ext:', AmqpExtConnectionFactory::class];
+
+        yield ['amqp+rabbitmq:', AmqpBunnyConnectionFactory::class];
+
+        yield ['amqp+rabbitmq+bunny:', AmqpBunnyConnectionFactory::class];
+
+        yield ['amqp+foo+bar+lib:', AmqpLibConnectionFactory::class];
+
+        yield ['amqp+rabbitmq+ext:', AmqpExtConnectionFactory::class];
+
+        yield ['amqp+rabbitmq+lib:', AmqpLibConnectionFactory::class];
+
+        // bunny does not support amqps, so it is skipped
+        yield ['amqps:', AmqpExtConnectionFactory::class];
+
+        // bunny does not support amqps, so it is skipped
+        yield ['amqps+ext:', AmqpExtConnectionFactory::class];
+
+        // bunny does not support amqps, so it is skipped
+        yield ['amqps+rabbitmq:', AmqpExtConnectionFactory::class];
+
+        yield ['amqps+ext+rabbitmq:', AmqpExtConnectionFactory::class];
+
+        yield ['amqps+lib+rabbitmq:', AmqpLibConnectionFactory::class];
+
+        yield ['mssql:', DbalConnectionFactory::class];
+
+        yield ['mysql:', DbalConnectionFactory::class];
+
+        yield ['pgsql:', DbalConnectionFactory::class];
+
+        yield ['file:', FsConnectionFactory::class];
+
+        // https://github.com/php-enqueue/enqueue-dev/issues/511
+        //        yield ['gearman:', GearmanConnectionFactory::class];
+
+        yield ['gps:', GpsConnectionFactory::class];
+
+        yield ['mongodb:', MongodbConnectionFactory::class];
+
+        yield ['beanstalk:', PheanstalkConnectionFactory::class];
+
+        yield ['kafka:', RdKafkaConnectionFactory::class];
+
+        yield ['redis:', RedisConnectionFactory::class];
+
+        yield ['redis+predis:', RedisConnectionFactory::class];
+
+        yield ['redis+foo+bar+phpredis:', RedisConnectionFactory::class];
+
+        yield ['redis+phpredis:', RedisConnectionFactory::class];
+
+        yield ['sqs:', SqsConnectionFactory::class];
+
+        yield ['stomp:', StompConnectionFactory::class];
+    }
+}
diff --git a/Tests/Consumption/CallbackProcessorTest.php b/Tests/Consumption/CallbackProcessorTest.php
index 1b034d9..86adbd3 100644
--- a/Tests/Consumption/CallbackProcessorTest.php
+++ b/Tests/Consumption/CallbackProcessorTest.php
@@ -6,7 +6,7 @@
 use Enqueue\Null\NullContext;
 use Enqueue\Null\NullMessage;
 use Enqueue\Test\ClassExtensionTrait;
-use Interop\Queue\PsrProcessor;
+use Interop\Queue\Processor;
 use PHPUnit\Framework\TestCase;
 
 class CallbackProcessorTest extends TestCase
@@ -15,13 +15,7 @@ class CallbackProcessorTest extends TestCase
 
     public function testShouldImplementProcessorInterface()
     {
-        $this->assertClassImplements(PsrProcessor::class, CallbackProcessor::class);
-    }
-
-    public function testCouldBeConstructedWithCallableAsArgument()
-    {
-        new CallbackProcessor(function () {
-        });
+        $this->assertClassImplements(Processor::class, CallbackProcessor::class);
     }
 
     public function testShouldCallCallbackAndProxyItsReturnedValue()
diff --git a/Tests/Consumption/ChainExtensionTest.php b/Tests/Consumption/ChainExtensionTest.php
index 4c412c6..198d000 100644
--- a/Tests/Consumption/ChainExtensionTest.php
+++ b/Tests/Consumption/ChainExtensionTest.php
@@ -3,10 +3,26 @@
 namespace Enqueue\Tests\Consumption;
 
 use Enqueue\Consumption\ChainExtension;
-use Enqueue\Consumption\Context;
+use Enqueue\Consumption\Context\End;
+use Enqueue\Consumption\Context\InitLogger;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\Context\MessageResult;
+use Enqueue\Consumption\Context\PostConsume;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\PreConsume;
+use Enqueue\Consumption\Context\PreSubscribe;
+use Enqueue\Consumption\Context\Start;
 use Enqueue\Consumption\ExtensionInterface;
 use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Message;
+use Interop\Queue\Processor;
+use Interop\Queue\SubscriptionConsumer;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 
 class ChainExtensionTest extends TestCase
 {
@@ -17,14 +33,31 @@ public function testShouldImplementExtensionInterface()
         $this->assertClassImplements(ExtensionInterface::class, ChainExtension::class);
     }
 
-    public function testCouldBeConstructedWithExtensionsArray()
+    public function testShouldProxyOnInitLoggerToAllInternalExtensions()
     {
-        new ChainExtension([$this->createExtension(), $this->createExtension()]);
+        $context = new InitLogger(new NullLogger());
+
+        $fooExtension = $this->createExtension();
+        $fooExtension
+            ->expects($this->once())
+            ->method('onInitLogger')
+            ->with($this->identicalTo($context))
+        ;
+        $barExtension = $this->createExtension();
+        $barExtension
+            ->expects($this->once())
+            ->method('onInitLogger')
+            ->with($this->identicalTo($context))
+        ;
+
+        $extensions = new ChainExtension([$fooExtension, $barExtension]);
+
+        $extensions->onInitLogger($context);
     }
 
     public function testShouldProxyOnStartToAllInternalExtensions()
     {
-        $context = $this->createContextMock();
+        $context = new Start($this->createInteropContextMock(), $this->createLoggerMock(), [], 0, 0);
 
         $fooExtension = $this->createExtension();
         $fooExtension
@@ -44,53 +77,100 @@ public function testShouldProxyOnStartToAllInternalExtensions()
         $extensions->onStart($context);
     }
 
-    public function testShouldProxyOnBeforeReceiveToAllInternalExtensions()
+    public function testShouldProxyOnPreSubscribeToAllInternalExtensions()
     {
-        $context = $this->createContextMock();
+        $context = new PreSubscribe(
+            $this->createInteropContextMock(),
+            $this->createInteropProcessorMock(),
+            $this->createInteropConsumerMock(),
+            $this->createLoggerMock()
+        );
 
         $fooExtension = $this->createExtension();
         $fooExtension
             ->expects($this->once())
-            ->method('onBeforeReceive')
+            ->method('onPreSubscribe')
             ->with($this->identicalTo($context))
         ;
         $barExtension = $this->createExtension();
         $barExtension
             ->expects($this->once())
-            ->method('onBeforeReceive')
+            ->method('onPreSubscribe')
             ->with($this->identicalTo($context))
         ;
 
         $extensions = new ChainExtension([$fooExtension, $barExtension]);
 
-        $extensions->onBeforeReceive($context);
+        $extensions->onPreSubscribe($context);
+    }
+
+    public function testShouldProxyOnPreConsumeToAllInternalExtensions()
+    {
+        $context = new PreConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            new NullLogger(),
+            1,
+            2,
+            3
+        );
+
+        $fooExtension = $this->createExtension();
+        $fooExtension
+            ->expects($this->once())
+            ->method('onPreConsume')
+            ->with($this->identicalTo($context))
+        ;
+        $barExtension = $this->createExtension();
+        $barExtension
+            ->expects($this->once())
+            ->method('onPreConsume')
+            ->with($this->identicalTo($context))
+        ;
+
+        $extensions = new ChainExtension([$fooExtension, $barExtension]);
+        $extensions->onPreConsume($context);
     }
 
     public function testShouldProxyOnPreReceiveToAllInternalExtensions()
     {
-        $context = $this->createContextMock();
+        $context = new MessageReceived(
+            $this->createInteropContextMock(),
+            $this->createMock(Consumer::class),
+            $this->createMock(Message::class),
+            $this->createMock(Processor::class),
+            1,
+            new NullLogger()
+        );
 
         $fooExtension = $this->createExtension();
         $fooExtension
             ->expects($this->once())
-            ->method('onPreReceived')
+            ->method('onMessageReceived')
             ->with($this->identicalTo($context))
         ;
         $barExtension = $this->createExtension();
         $barExtension
             ->expects($this->once())
-            ->method('onPreReceived')
+            ->method('onMessageReceived')
             ->with($this->identicalTo($context))
         ;
 
         $extensions = new ChainExtension([$fooExtension, $barExtension]);
 
-        $extensions->onPreReceived($context);
+        $extensions->onMessageReceived($context);
     }
 
     public function testShouldProxyOnResultToAllInternalExtensions()
     {
-        $context = $this->createContextMock();
+        $context = new MessageResult(
+            $this->createInteropContextMock(),
+            $this->createInteropConsumerMock(),
+            $this->createMock(Message::class),
+            'aResult',
+            1,
+            new NullLogger()
+        );
 
         $fooExtension = $this->createExtension();
         $fooExtension
@@ -112,83 +192,129 @@ public function testShouldProxyOnResultToAllInternalExtensions()
 
     public function testShouldProxyOnPostReceiveToAllInternalExtensions()
     {
-        $context = $this->createContextMock();
+        $context = new PostMessageReceived(
+            $this->createInteropContextMock(),
+            $this->createMock(Consumer::class),
+            $this->createMock(Message::class),
+            'aResult',
+            1,
+            new NullLogger()
+        );
 
         $fooExtension = $this->createExtension();
         $fooExtension
             ->expects($this->once())
-            ->method('onPostReceived')
+            ->method('onPostMessageReceived')
             ->with($this->identicalTo($context))
         ;
         $barExtension = $this->createExtension();
         $barExtension
             ->expects($this->once())
-            ->method('onPostReceived')
+            ->method('onPostMessageReceived')
             ->with($this->identicalTo($context))
         ;
 
         $extensions = new ChainExtension([$fooExtension, $barExtension]);
 
-        $extensions->onPostReceived($context);
+        $extensions->onPostMessageReceived($context);
     }
 
-    public function testShouldProxyOnIdleToAllInternalExtensions()
+    public function testShouldProxyOnPostConsumeToAllInternalExtensions()
     {
-        $context = $this->createContextMock();
+        $postConsume = new PostConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            1,
+            1,
+            1,
+            new NullLogger()
+        );
 
         $fooExtension = $this->createExtension();
         $fooExtension
             ->expects($this->once())
-            ->method('onIdle')
-            ->with($this->identicalTo($context))
+            ->method('onPostConsume')
+            ->with($this->identicalTo($postConsume))
         ;
         $barExtension = $this->createExtension();
         $barExtension
             ->expects($this->once())
-            ->method('onIdle')
-            ->with($this->identicalTo($context))
+            ->method('onPostConsume')
+            ->with($this->identicalTo($postConsume))
         ;
 
         $extensions = new ChainExtension([$fooExtension, $barExtension]);
 
-        $extensions->onIdle($context);
+        $extensions->onPostConsume($postConsume);
     }
 
-    public function testShouldProxyOnInterruptedToAllInternalExtensions()
+    public function testShouldProxyOnEndToAllInternalExtensions()
     {
-        $context = $this->createContextMock();
+        $context = new End($this->createInteropContextMock(), 1, 2, new NullLogger());
 
         $fooExtension = $this->createExtension();
         $fooExtension
             ->expects($this->once())
-            ->method('onInterrupted')
+            ->method('onEnd')
             ->with($this->identicalTo($context))
         ;
         $barExtension = $this->createExtension();
         $barExtension
             ->expects($this->once())
-            ->method('onInterrupted')
+            ->method('onEnd')
             ->with($this->identicalTo($context))
         ;
 
         $extensions = new ChainExtension([$fooExtension, $barExtension]);
 
-        $extensions->onInterrupted($context);
+        $extensions->onEnd($context);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|Context
+     * @return MockObject
      */
-    protected function createContextMock()
+    protected function createLoggerMock(): LoggerInterface
+    {
+        return $this->createMock(LoggerInterface::class);
+    }
+
+    /**
+     * @return MockObject
+     */
+    protected function createInteropContextMock(): Context
     {
         return $this->createMock(Context::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|ExtensionInterface
+     * @return MockObject
+     */
+    protected function createInteropConsumerMock(): Consumer
+    {
+        return $this->createMock(Consumer::class);
+    }
+
+    /**
+     * @return MockObject
+     */
+    protected function createInteropProcessorMock(): Processor
+    {
+        return $this->createMock(Processor::class);
+    }
+
+    /**
+     * @return MockObject|ExtensionInterface
      */
     protected function createExtension()
     {
         return $this->createMock(ExtensionInterface::class);
     }
+
+    /**
+     * @return MockObject
+     */
+    private function createSubscriptionConsumerMock(): SubscriptionConsumer
+    {
+        return $this->createMock(SubscriptionConsumer::class);
+    }
 }
diff --git a/Tests/Consumption/ContextTest.php b/Tests/Consumption/ContextTest.php
deleted file mode 100644
index a0f6b26..0000000
--- a/Tests/Consumption/ContextTest.php
+++ /dev/null
@@ -1,258 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Consumption;
-
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\Exception\IllegalContextModificationException;
-use Enqueue\Null\NullQueue;
-use Enqueue\Test\ClassExtensionTrait;
-use Interop\Queue\PsrConsumer;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrMessage;
-use Interop\Queue\PsrProcessor;
-use PHPUnit\Framework\TestCase;
-use Psr\Log\NullLogger;
-
-class ContextTest extends TestCase
-{
-    use ClassExtensionTrait;
-
-    public function testCouldBeConstructedWithSessionAsFirstArgument()
-    {
-        new Context($this->createPsrContext());
-    }
-
-    public function testShouldAllowGetSessionSetInConstructor()
-    {
-        $psrContext = $this->createPsrContext();
-
-        $context = new Context($psrContext);
-
-        $this->assertSame($psrContext, $context->getPsrContext());
-    }
-
-    public function testShouldAllowGetMessageConsumerPreviouslySet()
-    {
-        $messageConsumer = $this->createPsrConsumer();
-
-        $context = new Context($this->createPsrContext());
-        $context->setPsrConsumer($messageConsumer);
-
-        $this->assertSame($messageConsumer, $context->getPsrConsumer());
-    }
-
-    public function testThrowOnTryToChangeMessageConsumerIfAlreadySet()
-    {
-        $messageConsumer = $this->createPsrConsumer();
-        $anotherMessageConsumer = $this->createPsrConsumer();
-
-        $context = new Context($this->createPsrContext());
-
-        $context->setPsrConsumer($messageConsumer);
-
-        $this->expectException(IllegalContextModificationException::class);
-
-        $context->setPsrConsumer($anotherMessageConsumer);
-    }
-
-    public function testShouldAllowGetMessageProducerPreviouslySet()
-    {
-        $processorMock = $this->createProcessorMock();
-
-        $context = new Context($this->createPsrContext());
-        $context->setPsrProcessor($processorMock);
-
-        $this->assertSame($processorMock, $context->getPsrProcessor());
-    }
-
-    public function testThrowOnTryToChangeProcessorIfAlreadySet()
-    {
-        $processor = $this->createProcessorMock();
-        $anotherProcessor = $this->createProcessorMock();
-
-        $context = new Context($this->createPsrContext());
-
-        $context->setPsrProcessor($processor);
-
-        $this->expectException(IllegalContextModificationException::class);
-
-        $context->setPsrProcessor($anotherProcessor);
-    }
-
-    public function testShouldAllowGetLoggerPreviouslySet()
-    {
-        $logger = new NullLogger();
-
-        $context = new Context($this->createPsrContext());
-        $context->setLogger($logger);
-
-        $this->assertSame($logger, $context->getLogger());
-    }
-
-    public function testShouldSetExecutionInterruptedToFalseInConstructor()
-    {
-        $context = new Context($this->createPsrContext());
-
-        $this->assertFalse($context->isExecutionInterrupted());
-    }
-
-    public function testShouldAllowGetPreviouslySetMessage()
-    {
-        /** @var PsrMessage $message */
-        $message = $this->createMock(PsrMessage::class);
-
-        $context = new Context($this->createPsrContext());
-
-        $context->setPsrMessage($message);
-
-        $this->assertSame($message, $context->getPsrMessage());
-    }
-
-    public function testThrowOnTryToChangeMessageIfAlreadySet()
-    {
-        /** @var PsrMessage $message */
-        $message = $this->createMock(PsrMessage::class);
-
-        $context = new Context($this->createPsrContext());
-
-        $this->expectException(IllegalContextModificationException::class);
-        $this->expectExceptionMessage('The message could be set once');
-
-        $context->setPsrMessage($message);
-        $context->setPsrMessage($message);
-    }
-
-    public function testShouldAllowGetPreviouslySetException()
-    {
-        $exception = new \Exception();
-
-        $context = new Context($this->createPsrContext());
-
-        $context->setException($exception);
-
-        $this->assertSame($exception, $context->getException());
-    }
-
-    public function testShouldAllowGetPreviouslySetResult()
-    {
-        $result = 'aResult';
-
-        $context = new Context($this->createPsrContext());
-
-        $context->setResult($result);
-
-        $this->assertSame($result, $context->getResult());
-    }
-
-    public function testThrowOnTryToChangeResultIfAlreadySet()
-    {
-        $result = 'aResult';
-
-        $context = new Context($this->createPsrContext());
-
-        $this->expectException(IllegalContextModificationException::class);
-        $this->expectExceptionMessage('The result modification is not allowed');
-
-        $context->setResult($result);
-        $context->setResult($result);
-    }
-
-    public function testShouldAllowGetPreviouslySetExecutionInterrupted()
-    {
-        $context = new Context($this->createPsrContext());
-
-        // guard
-        $this->assertFalse($context->isExecutionInterrupted());
-
-        $context->setExecutionInterrupted(true);
-
-        $this->assertTrue($context->isExecutionInterrupted());
-    }
-
-    public function testThrowOnTryToRollbackExecutionInterruptedIfAlreadySetToTrue()
-    {
-        $context = new Context($this->createPsrContext());
-
-        $this->expectException(IllegalContextModificationException::class);
-        $this->expectExceptionMessage('The execution once interrupted could not be roll backed');
-
-        $context->setExecutionInterrupted(true);
-        $context->setExecutionInterrupted(false);
-    }
-
-    public function testNotThrowOnSettingExecutionInterruptedToTrueIfAlreadySetToTrue()
-    {
-        $context = new Context($this->createPsrContext());
-
-        $context->setExecutionInterrupted(true);
-        $context->setExecutionInterrupted(true);
-    }
-
-    public function testShouldAllowGetPreviouslySetLogger()
-    {
-        $expectedLogger = new NullLogger();
-
-        $context = new Context($this->createPsrContext());
-
-        $context->setLogger($expectedLogger);
-
-        $this->assertSame($expectedLogger, $context->getLogger());
-    }
-
-    public function testThrowOnSettingLoggerIfAlreadySet()
-    {
-        $context = new Context($this->createPsrContext());
-
-        $context->setLogger(new NullLogger());
-
-        $this->expectException(IllegalContextModificationException::class);
-        $this->expectExceptionMessage('The logger modification is not allowed');
-
-        $context->setLogger(new NullLogger());
-    }
-
-    public function testShouldAllowGetPreviouslySetQueue()
-    {
-        $context = new Context($this->createPsrContext());
-
-        $context->setPsrQueue($queue = new NullQueue(''));
-
-        $this->assertSame($queue, $context->getPsrQueue());
-    }
-
-    public function testThrowOnSettingQueueNameIfAlreadySet()
-    {
-        $context = new Context($this->createPsrContext());
-
-        $context->setPsrQueue(new NullQueue(''));
-
-        $this->expectException(IllegalContextModificationException::class);
-        $this->expectExceptionMessage('The queue modification is not allowed');
-
-        $context->setPsrQueue(new NullQueue(''));
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext
-     */
-    protected function createPsrContext()
-    {
-        return $this->createMock(PsrContext::class);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrConsumer
-     */
-    protected function createPsrConsumer()
-    {
-        return $this->createMock(PsrConsumer::class);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor
-     */
-    protected function createProcessorMock()
-    {
-        return $this->createMock(PsrProcessor::class);
-    }
-}
diff --git a/Tests/Consumption/EmptyExtensionTraitTest.php b/Tests/Consumption/EmptyExtensionTraitTest.php
deleted file mode 100644
index 32ea861..0000000
--- a/Tests/Consumption/EmptyExtensionTraitTest.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Consumption;
-
-use Enqueue\Consumption\EmptyExtensionTrait;
-use Enqueue\Consumption\ExtensionInterface;
-use PHPUnit\Framework\TestCase;
-
-class EmptyExtensionTraitTest extends TestCase
-{
-    public function testTraitMustImplementOrExtensionMethods()
-    {
-        new EmptyExtension();
-    }
-}
-
-class EmptyExtension implements ExtensionInterface
-{
-    use EmptyExtensionTrait;
-}
diff --git a/Tests/Consumption/Exception/ConsumptionInterruptedExceptionTest.php b/Tests/Consumption/Exception/ConsumptionInterruptedExceptionTest.php
deleted file mode 100644
index c6f608b..0000000
--- a/Tests/Consumption/Exception/ConsumptionInterruptedExceptionTest.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Consumption\Exception;
-
-use Enqueue\Consumption\Exception\ConsumptionInterruptedException;
-use Enqueue\Consumption\Exception\ExceptionInterface;
-use Enqueue\Test\ClassExtensionTrait;
-use PHPUnit\Framework\TestCase;
-
-class ConsumptionInterruptedExceptionTest extends TestCase
-{
-    use ClassExtensionTrait;
-
-    public function testShouldImplementExceptionInterface()
-    {
-        $this->assertClassImplements(ExceptionInterface::class, ConsumptionInterruptedException::class);
-    }
-
-    public function testShouldExtendLogicException()
-    {
-        $this->assertClassExtends(\LogicException::class, ConsumptionInterruptedException::class);
-    }
-
-    public function testCouldBeConstructedWithoutAnyArguments()
-    {
-        new ConsumptionInterruptedException();
-    }
-}
diff --git a/Tests/Consumption/Exception/IllegalContextModificationExceptionTest.php b/Tests/Consumption/Exception/IllegalContextModificationExceptionTest.php
index 0885b50..241f4ad 100644
--- a/Tests/Consumption/Exception/IllegalContextModificationExceptionTest.php
+++ b/Tests/Consumption/Exception/IllegalContextModificationExceptionTest.php
@@ -20,9 +20,4 @@ public function testShouldExtendLogicException()
     {
         $this->assertClassExtends(\LogicException::class, IllegalContextModificationException::class);
     }
-
-    public function testCouldBeConstructedWithoutAnyArguments()
-    {
-        new IllegalContextModificationException();
-    }
 }
diff --git a/Tests/Consumption/Exception/InvalidArgumentExceptionTest.php b/Tests/Consumption/Exception/InvalidArgumentExceptionTest.php
index 296c762..c1c5db3 100644
--- a/Tests/Consumption/Exception/InvalidArgumentExceptionTest.php
+++ b/Tests/Consumption/Exception/InvalidArgumentExceptionTest.php
@@ -21,11 +21,6 @@ public function testShouldExtendLogicException()
         $this->assertClassExtends(\LogicException::class, InvalidArgumentException::class);
     }
 
-    public function testCouldBeConstructedWithoutAnyArguments()
-    {
-        new InvalidArgumentException();
-    }
-
     public function testThrowIfAssertInstanceOfNotSameAsExpected()
     {
         $this->expectException(InvalidArgumentException::class);
@@ -36,6 +31,9 @@ public function testThrowIfAssertInstanceOfNotSameAsExpected()
         InvalidArgumentException::assertInstanceOf(new \SplStack(), \SplQueue::class);
     }
 
+    /**
+     * @doesNotPerformAssertions
+     */
     public function testShouldDoNothingIfAssertDestinationInstanceOfSameAsExpected()
     {
         InvalidArgumentException::assertInstanceOf(new \SplQueue(), \SplQueue::class);
diff --git a/Tests/Consumption/Exception/LogicExceptionTest.php b/Tests/Consumption/Exception/LogicExceptionTest.php
index ddd2580..2655609 100644
--- a/Tests/Consumption/Exception/LogicExceptionTest.php
+++ b/Tests/Consumption/Exception/LogicExceptionTest.php
@@ -20,9 +20,4 @@ public function testShouldExtendLogicException()
     {
         $this->assertClassExtends(\LogicException::class, LogicException::class);
     }
-
-    public function testCouldBeConstructedWithoutAnyArguments()
-    {
-        new LogicException();
-    }
 }
diff --git a/Tests/Consumption/Extension/LimitConsumedMessagesExtensionTest.php b/Tests/Consumption/Extension/LimitConsumedMessagesExtensionTest.php
index 250c577..137e30b 100644
--- a/Tests/Consumption/Extension/LimitConsumedMessagesExtensionTest.php
+++ b/Tests/Consumption/Extension/LimitConsumedMessagesExtensionTest.php
@@ -2,41 +2,84 @@
 
 namespace Enqueue\Tests\Consumption\Extension;
 
-use Enqueue\Consumption\Context;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\PreConsume;
 use Enqueue\Consumption\Extension\LimitConsumedMessagesExtension;
-use Interop\Queue\PsrConsumer;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrProcessor;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Message;
+use Interop\Queue\SubscriptionConsumer;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 
 class LimitConsumedMessagesExtensionTest extends TestCase
 {
-    public function testCouldBeConstructedWithRequiredArguments()
+    public function testOnPreConsumeShouldInterruptWhenLimitIsReached()
     {
-        new LimitConsumedMessagesExtension(12345);
-    }
+        $logger = $this->createLoggerMock();
+        $logger
+            ->expects($this->once())
+            ->method('debug')
+            ->with('[LimitConsumedMessagesExtension] Message consumption is interrupted since'.
+                ' the message limit reached. limit: "3"')
+        ;
 
-    public function testShouldThrowExceptionIfMessageLimitIsNotInt()
-    {
-        $this->setExpectedException(
-            \InvalidArgumentException::class,
-            'Expected message limit is int but got: "double"'
+        $context = new PreConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            $logger,
+            1,
+            2,
+            3
+        );
+
+        // guard
+        $this->assertFalse($context->isExecutionInterrupted());
+
+        // test
+        $extension = new LimitConsumedMessagesExtension(3);
+
+        $extension->onPreConsume($context);
+        $this->assertFalse($context->isExecutionInterrupted());
+
+        $postReceivedMessage = new PostMessageReceived(
+            $this->createInteropContextMock(),
+            $this->createMock(Consumer::class),
+            $this->createMock(Message::class),
+            'aResult',
+            1,
+            new NullLogger()
         );
 
-        new LimitConsumedMessagesExtension(0.0);
+        $extension->onPostMessageReceived($postReceivedMessage);
+        $extension->onPostMessageReceived($postReceivedMessage);
+        $extension->onPostMessageReceived($postReceivedMessage);
+
+        $extension->onPreConsume($context);
+        $this->assertTrue($context->isExecutionInterrupted());
     }
 
-    public function testOnBeforeReceiveShouldInterruptExecutionIfLimitIsZero()
+    public function testOnPreConsumeShouldInterruptExecutionIfLimitIsZero()
     {
-        $context = $this->createContext();
-        $context->getLogger()
+        $logger = $this->createLoggerMock();
+        $logger
             ->expects($this->once())
             ->method('debug')
             ->with('[LimitConsumedMessagesExtension] Message consumption is interrupted since'.
                 ' the message limit reached. limit: "0"')
         ;
 
+        $context = new PreConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            $logger,
+            1,
+            2,
+            3
+        );
+
         // guard
         $this->assertFalse($context->isExecutionInterrupted());
 
@@ -44,20 +87,29 @@ public function testOnBeforeReceiveShouldInterruptExecutionIfLimitIsZero()
         $extension = new LimitConsumedMessagesExtension(0);
 
         // consume 1
-        $extension->onBeforeReceive($context);
+        $extension->onPreConsume($context);
         $this->assertTrue($context->isExecutionInterrupted());
     }
 
-    public function testOnBeforeReceiveShouldInterruptExecutionIfLimitIsLessThatZero()
+    public function testOnPreConsumeShouldInterruptExecutionIfLimitIsLessThatZero()
     {
-        $context = $this->createContext();
-        $context->getLogger()
+        $logger = $this->createLoggerMock();
+        $logger
             ->expects($this->once())
             ->method('debug')
             ->with('[LimitConsumedMessagesExtension] Message consumption is interrupted since'.
                 ' the message limit reached. limit: "-1"')
         ;
 
+        $context = new PreConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            $logger,
+            1,
+            2,
+            3
+        );
+
         // guard
         $this->assertFalse($context->isExecutionInterrupted());
 
@@ -65,45 +117,65 @@ public function testOnBeforeReceiveShouldInterruptExecutionIfLimitIsLessThatZero
         $extension = new LimitConsumedMessagesExtension(-1);
 
         // consume 1
-        $extension->onBeforeReceive($context);
+        $extension->onPreConsume($context);
         $this->assertTrue($context->isExecutionInterrupted());
     }
 
     public function testOnPostReceivedShouldInterruptExecutionIfMessageLimitExceeded()
     {
-        $context = $this->createContext();
-        $context->getLogger()
+        $logger = $this->createLoggerMock();
+        $logger
             ->expects($this->once())
             ->method('debug')
             ->with('[LimitConsumedMessagesExtension] Message consumption is interrupted since'.
                 ' the message limit reached. limit: "2"')
         ;
 
+        $postReceivedMessage = new PostMessageReceived(
+            $this->createInteropContextMock(),
+            $this->createMock(Consumer::class),
+            $this->createMock(Message::class),
+            'aResult',
+            1,
+            $logger
+        );
+
         // guard
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postReceivedMessage->isExecutionInterrupted());
 
         // test
         $extension = new LimitConsumedMessagesExtension(2);
 
         // consume 1
-        $extension->onPostReceived($context);
-        $this->assertFalse($context->isExecutionInterrupted());
+        $extension->onPostMessageReceived($postReceivedMessage);
+        $this->assertFalse($postReceivedMessage->isExecutionInterrupted());
 
         // consume 2 and exit
-        $extension->onPostReceived($context);
-        $this->assertTrue($context->isExecutionInterrupted());
+        $extension->onPostMessageReceived($postReceivedMessage);
+        $this->assertTrue($postReceivedMessage->isExecutionInterrupted());
     }
 
     /**
-     * @return Context
+     * @return MockObject
      */
-    protected function createContext()
+    protected function createInteropContextMock(): Context
     {
-        $context = new Context($this->createMock(PsrContext::class));
-        $context->setLogger($this->createMock(LoggerInterface::class));
-        $context->setPsrConsumer($this->createMock(PsrConsumer::class));
-        $context->setPsrProcessor($this->createMock(PsrProcessor::class));
+        return $this->createMock(Context::class);
+    }
 
-        return $context;
+    /**
+     * @return MockObject
+     */
+    private function createSubscriptionConsumerMock(): SubscriptionConsumer
+    {
+        return $this->createMock(SubscriptionConsumer::class);
+    }
+
+    /**
+     * @return MockObject
+     */
+    private function createLoggerMock(): LoggerInterface
+    {
+        return $this->createMock(LoggerInterface::class);
     }
 }
diff --git a/Tests/Consumption/Extension/LimitConsumerMemoryExtensionTest.php b/Tests/Consumption/Extension/LimitConsumerMemoryExtensionTest.php
index 74514e5..25ac858 100644
--- a/Tests/Consumption/Extension/LimitConsumerMemoryExtensionTest.php
+++ b/Tests/Consumption/Extension/LimitConsumerMemoryExtensionTest.php
@@ -2,137 +2,197 @@
 
 namespace Enqueue\Tests\Consumption\Extension;
 
-use Enqueue\Consumption\Context;
+use Enqueue\Consumption\Context\PostConsume;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\PreConsume;
 use Enqueue\Consumption\Extension\LimitConsumerMemoryExtension;
-use Interop\Queue\PsrConsumer;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrProcessor;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Message;
+use Interop\Queue\SubscriptionConsumer;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 
 class LimitConsumerMemoryExtensionTest extends TestCase
 {
-    public function testCouldBeConstructedWithRequiredArguments()
-    {
-        new LimitConsumerMemoryExtension(12345);
-    }
-
     public function testShouldThrowExceptionIfMemoryLimitIsNotInt()
     {
-        $this->setExpectedException(\InvalidArgumentException::class, 'Expected memory limit is int but got: "double"');
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Expected memory limit is int but got: "double"');
 
         new LimitConsumerMemoryExtension(0.0);
     }
 
-    public function testOnIdleShouldInterruptExecutionIfMemoryLimitReached()
+    public function testOnPostConsumeShouldInterruptExecutionIfMemoryLimitReached()
     {
-        $context = $this->createPsrContext();
-        $context->getLogger()
+        $logger = $this->createLoggerMock();
+        $logger
             ->expects($this->once())
             ->method('debug')
             ->with($this->stringContains('[LimitConsumerMemoryExtension] Interrupt execution as memory limit reached.'))
         ;
 
+        $postConsume = new PostConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            1,
+            1,
+            1,
+            $logger
+        );
+
         // guard
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postConsume->isExecutionInterrupted());
 
         // test
         $extension = new LimitConsumerMemoryExtension(1);
-        $extension->onIdle($context);
+        $extension->onPostConsume($postConsume);
 
-        $this->assertTrue($context->isExecutionInterrupted());
+        $this->assertTrue($postConsume->isExecutionInterrupted());
     }
 
     public function testOnPostReceivedShouldInterruptExecutionIfMemoryLimitReached()
     {
-        $context = $this->createPsrContext();
-        $context->getLogger()
+        $logger = $this->createLoggerMock();
+        $logger
             ->expects($this->once())
             ->method('debug')
             ->with($this->stringContains('[LimitConsumerMemoryExtension] Interrupt execution as memory limit reached.'))
         ;
 
+        $postReceivedMessage = new PostMessageReceived(
+            $this->createInteropContextMock(),
+            $this->createMock(Consumer::class),
+            $this->createMock(Message::class),
+            'aResult',
+            1,
+            $logger
+        );
+
         // guard
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postReceivedMessage->isExecutionInterrupted());
 
         // test
         $extension = new LimitConsumerMemoryExtension(1);
-        $extension->onPostReceived($context);
+        $extension->onPostMessageReceived($postReceivedMessage);
 
-        $this->assertTrue($context->isExecutionInterrupted());
+        $this->assertTrue($postReceivedMessage->isExecutionInterrupted());
     }
 
-    public function testOnBeforeReceivedShouldInterruptExecutionIfMemoryLimitReached()
+    public function testOnPreConsumeShouldInterruptExecutionIfMemoryLimitReached()
     {
-        $context = $this->createPsrContext();
-        $context->getLogger()
+        $logger = $this->createLoggerMock();
+        $logger
             ->expects($this->once())
             ->method('debug')
             ->with($this->stringContains('[LimitConsumerMemoryExtension] Interrupt execution as memory limit reached.'))
         ;
 
+        $context = new PreConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            $logger,
+            1,
+            2,
+            3
+        );
+
         // guard
         $this->assertFalse($context->isExecutionInterrupted());
 
         // test
         $extension = new LimitConsumerMemoryExtension(1);
-        $extension->onBeforeReceive($context);
+        $extension->onPreConsume($context);
 
         $this->assertTrue($context->isExecutionInterrupted());
     }
 
-    public function testOnBeforeReceiveShouldNotInterruptExecutionIfMemoryLimitIsNotReached()
+    public function testOnPreConsumeShouldNotInterruptExecutionIfMemoryLimitIsNotReached()
     {
-        $context = $this->createPsrContext();
+        $context = new PreConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            new NullLogger(),
+            1,
+            2,
+            3
+        );
 
         // guard
         $this->assertFalse($context->isExecutionInterrupted());
 
         // test
-        $extension = new LimitConsumerMemoryExtension(PHP_INT_MAX);
-        $extension->onBeforeReceive($context);
+        $extension = new LimitConsumerMemoryExtension(\PHP_INT_MAX);
+        $extension->onPreConsume($context);
 
         $this->assertFalse($context->isExecutionInterrupted());
     }
 
-    public function testOnIdleShouldNotInterruptExecutionIfMemoryLimitIsNotReached()
+    public function testOnPostConsumeShouldNotInterruptExecutionIfMemoryLimitIsNotReached()
     {
-        $context = $this->createPsrContext();
+        $postConsume = new PostConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            1,
+            1,
+            1,
+            new NullLogger()
+        );
 
         // guard
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postConsume->isExecutionInterrupted());
 
         // test
-        $extension = new LimitConsumerMemoryExtension(PHP_INT_MAX);
-        $extension->onIdle($context);
+        $extension = new LimitConsumerMemoryExtension(\PHP_INT_MAX);
+        $extension->onPostConsume($postConsume);
 
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postConsume->isExecutionInterrupted());
     }
 
-    public function testOnPostReceivedShouldNotInterruptExecutionIfMemoryLimitIsNotReached()
+    public function testOnPostMessageReceivedShouldNotInterruptExecutionIfMemoryLimitIsNotReached()
     {
-        $context = $this->createPsrContext();
+        $postReceivedMessage = new PostMessageReceived(
+            $this->createInteropContextMock(),
+            $this->createMock(Consumer::class),
+            $this->createMock(Message::class),
+            'aResult',
+            1,
+            new NullLogger()
+        );
 
         // guard
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postReceivedMessage->isExecutionInterrupted());
 
         // test
-        $extension = new LimitConsumerMemoryExtension(PHP_INT_MAX);
-        $extension->onPostReceived($context);
+        $extension = new LimitConsumerMemoryExtension(\PHP_INT_MAX);
+        $extension->onPostMessageReceived($postReceivedMessage);
 
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postReceivedMessage->isExecutionInterrupted());
     }
 
     /**
-     * @return Context
+     * @return MockObject
      */
-    protected function createPsrContext()
+    protected function createInteropContextMock(): Context
     {
-        $context = new Context($this->createMock(PsrContext::class));
-        $context->setLogger($this->createMock(LoggerInterface::class));
-        $context->setPsrConsumer($this->createMock(PsrConsumer::class));
-        $context->setPsrProcessor($this->createMock(PsrProcessor::class));
+        return $this->createMock(Context::class);
+    }
 
-        return $context;
+    /**
+     * @return MockObject
+     */
+    private function createSubscriptionConsumerMock(): SubscriptionConsumer
+    {
+        return $this->createMock(SubscriptionConsumer::class);
+    }
+
+    /**
+     * @return MockObject
+     */
+    private function createLoggerMock(): LoggerInterface
+    {
+        return $this->createMock(LoggerInterface::class);
     }
 }
diff --git a/Tests/Consumption/Extension/LimitConsumptionTimeExtensionTest.php b/Tests/Consumption/Extension/LimitConsumptionTimeExtensionTest.php
index fffeb94..fa6cb76 100644
--- a/Tests/Consumption/Extension/LimitConsumptionTimeExtensionTest.php
+++ b/Tests/Consumption/Extension/LimitConsumptionTimeExtensionTest.php
@@ -2,24 +2,31 @@
 
 namespace Enqueue\Tests\Consumption\Extension;
 
-use Enqueue\Consumption\Context;
+use Enqueue\Consumption\Context\PostConsume;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\PreConsume;
 use Enqueue\Consumption\Extension\LimitConsumptionTimeExtension;
-use Interop\Queue\PsrConsumer;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrProcessor;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Message;
+use Interop\Queue\SubscriptionConsumer;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 
 class LimitConsumptionTimeExtensionTest extends TestCase
 {
-    public function testCouldBeConstructedWithRequiredArguments()
+    public function testOnPreConsumeShouldInterruptExecutionIfConsumptionTimeExceeded()
     {
-        new LimitConsumptionTimeExtension(new \DateTime('+1 day'));
-    }
-
-    public function testOnBeforeReceiveShouldInterruptExecutionIfConsumptionTimeExceeded()
-    {
-        $context = $this->createContext();
+        $context = new PreConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            new NullLogger(),
+            1,
+            2,
+            3
+        );
 
         // guard
         $this->assertFalse($context->isExecutionInterrupted());
@@ -27,44 +34,65 @@ public function testOnBeforeReceiveShouldInterruptExecutionIfConsumptionTimeExce
         // test
         $extension = new LimitConsumptionTimeExtension(new \DateTime('-2 second'));
 
-        $extension->onBeforeReceive($context);
+        $extension->onPreConsume($context);
 
         $this->assertTrue($context->isExecutionInterrupted());
     }
 
-    public function testOnIdleShouldInterruptExecutionIfConsumptionTimeExceeded()
+    public function testOnPostConsumeShouldInterruptExecutionIfConsumptionTimeExceeded()
     {
-        $context = $this->createContext();
+        $postConsume = new PostConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            1,
+            1,
+            1,
+            new NullLogger()
+        );
 
         // guard
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postConsume->isExecutionInterrupted());
 
         // test
         $extension = new LimitConsumptionTimeExtension(new \DateTime('-2 second'));
 
-        $extension->onIdle($context);
+        $extension->onPostConsume($postConsume);
 
-        $this->assertTrue($context->isExecutionInterrupted());
+        $this->assertTrue($postConsume->isExecutionInterrupted());
     }
 
     public function testOnPostReceivedShouldInterruptExecutionIfConsumptionTimeExceeded()
     {
-        $context = $this->createContext();
+        $postReceivedMessage = new PostMessageReceived(
+            $this->createInteropContextMock(),
+            $this->createMock(Consumer::class),
+            $this->createMock(Message::class),
+            'aResult',
+            1,
+            new NullLogger()
+        );
 
         // guard
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postReceivedMessage->isExecutionInterrupted());
 
         // test
         $extension = new LimitConsumptionTimeExtension(new \DateTime('-2 second'));
 
-        $extension->onPostReceived($context);
+        $extension->onPostMessageReceived($postReceivedMessage);
 
-        $this->assertTrue($context->isExecutionInterrupted());
+        $this->assertTrue($postReceivedMessage->isExecutionInterrupted());
     }
 
-    public function testOnBeforeReceiveShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded()
+    public function testOnPreConsumeShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded()
     {
-        $context = $this->createContext();
+        $context = new PreConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            new NullLogger(),
+            1,
+            2,
+            3
+        );
 
         // guard
         $this->assertFalse($context->isExecutionInterrupted());
@@ -72,51 +100,76 @@ public function testOnBeforeReceiveShouldNotInterruptExecutionIfConsumptionTimeI
         // test
         $extension = new LimitConsumptionTimeExtension(new \DateTime('+2 second'));
 
-        $extension->onBeforeReceive($context);
+        $extension->onPreConsume($context);
 
         $this->assertFalse($context->isExecutionInterrupted());
     }
 
-    public function testOnIdleShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded()
+    public function testOnPostConsumeShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded()
     {
-        $context = $this->createContext();
+        $postConsume = new PostConsume(
+            $this->createInteropContextMock(),
+            $this->createSubscriptionConsumerMock(),
+            1,
+            1,
+            1,
+            new NullLogger()
+        );
 
         // guard
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postConsume->isExecutionInterrupted());
 
         // test
         $extension = new LimitConsumptionTimeExtension(new \DateTime('+2 second'));
 
-        $extension->onIdle($context);
+        $extension->onPostConsume($postConsume);
 
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postConsume->isExecutionInterrupted());
     }
 
     public function testOnPostReceivedShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded()
     {
-        $context = $this->createContext();
+        $postReceivedMessage = new PostMessageReceived(
+            $this->createInteropContextMock(),
+            $this->createMock(Consumer::class),
+            $this->createMock(Message::class),
+            'aResult',
+            1,
+            new NullLogger()
+        );
 
         // guard
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postReceivedMessage->isExecutionInterrupted());
 
         // test
         $extension = new LimitConsumptionTimeExtension(new \DateTime('+2 second'));
 
-        $extension->onPostReceived($context);
+        $extension->onPostMessageReceived($postReceivedMessage);
 
-        $this->assertFalse($context->isExecutionInterrupted());
+        $this->assertFalse($postReceivedMessage->isExecutionInterrupted());
+    }
+
+    /**
+     * @return MockObject
+     */
+    protected function createInteropContextMock(): Context
+    {
+        return $this->createMock(Context::class);
     }
 
     /**
-     * @return Context
+     * @return MockObject
      */
-    protected function createContext()
+    private function createSubscriptionConsumerMock(): SubscriptionConsumer
     {
-        $context = new Context($this->createMock(PsrContext::class));
-        $context->setLogger($this->createMock(LoggerInterface::class));
-        $context->setPsrConsumer($this->createMock(PsrConsumer::class));
-        $context->setPsrProcessor($this->createMock(PsrProcessor::class));
+        return $this->createMock(SubscriptionConsumer::class);
+    }
 
-        return $context;
+    /**
+     * @return MockObject
+     */
+    private function createLoggerMock(): LoggerInterface
+    {
+        return $this->createMock(LoggerInterface::class);
     }
 }
diff --git a/Tests/Consumption/Extension/LogExtensionTest.php b/Tests/Consumption/Extension/LogExtensionTest.php
new file mode 100644
index 0000000..006a2c5
--- /dev/null
+++ b/Tests/Consumption/Extension/LogExtensionTest.php
@@ -0,0 +1,266 @@
+<?php
+
+namespace Enqueue\Tests\Consumption\Extension;
+
+use Enqueue\Consumption\Context\End;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\Start;
+use Enqueue\Consumption\EndExtensionInterface;
+use Enqueue\Consumption\Extension\LogExtension;
+use Enqueue\Consumption\MessageReceivedExtensionInterface;
+use Enqueue\Consumption\PostMessageReceivedExtensionInterface;
+use Enqueue\Consumption\Result;
+use Enqueue\Consumption\StartExtensionInterface;
+use Enqueue\Null\NullMessage;
+use Enqueue\Null\NullQueue;
+use Enqueue\Test\ClassExtensionTrait;
+use Enqueue\Util\Stringify;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Processor;
+use Interop\Queue\Queue;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
+
+class LogExtensionTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementStartExtensionInterface()
+    {
+        $this->assertClassImplements(StartExtensionInterface::class, LogExtension::class);
+    }
+
+    public function testShouldImplementEndExtensionInterface()
+    {
+        $this->assertClassImplements(EndExtensionInterface::class, LogExtension::class);
+    }
+
+    public function testShouldImplementMessageReceivedExtensionInterface()
+    {
+        $this->assertClassImplements(MessageReceivedExtensionInterface::class, LogExtension::class);
+    }
+
+    public function testShouldImplementPostMessageReceivedExtensionInterface()
+    {
+        $this->assertClassImplements(PostMessageReceivedExtensionInterface::class, LogExtension::class);
+    }
+
+    public function testShouldLogStartOnStart()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('debug')
+            ->with('Consumption has started')
+        ;
+
+        $context = new Start($this->createContextMock(), $logger, [], 1, 1);
+
+        $extension = new LogExtension();
+        $extension->onStart($context);
+    }
+
+    public function testShouldLogEndOnEnd()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('debug')
+            ->with('Consumption has ended')
+        ;
+
+        $context = new End($this->createContextMock(), 1, 2, $logger);
+
+        $extension = new LogExtension();
+        $extension->onEnd($context);
+    }
+
+    public function testShouldLogMessageReceived()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('debug')
+            ->with('Received from {queueName}	{body}', [
+                'queueName' => 'aQueue',
+                'redelivered' => false,
+                'body' => Stringify::that('aBody'),
+                'properties' => Stringify::that(['aProp' => 'aPropVal']),
+                'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+            ])
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new MessageReceived($this->createContextMock(), $consumerMock, $message, $this->createProcessorMock(), 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onMessageReceived($context);
+    }
+
+    public function testShouldLogMessageProcessedWithStringResult()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::INFO,
+                'Processed from {queueName}	{body}	{result}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'aResult',
+                    'reason' => '',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, 'aResult', 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogRejectedMessageAsError()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::ERROR,
+                'Processed from {queueName}	{body}	{result}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'reject',
+                    'reason' => '',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Processor::REJECT, 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogMessageProcessedWithResultObject()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::INFO,
+                'Processed from {queueName}	{body}	{result}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'ack',
+                    'reason' => '',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack(), 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    public function testShouldLogMessageProcessedWithReasonResultObject()
+    {
+        $logger = $this->createLogger();
+        $logger
+            ->expects($this->once())
+            ->method('log')
+            ->with(LogLevel::INFO,
+                'Processed from {queueName}	{body}	{result} {reason}',
+                [
+                    'queueName' => 'aQueue',
+                    'body' => Stringify::that('aBody'),
+                    'properties' => Stringify::that(['aProp' => 'aPropVal']),
+                    'headers' => Stringify::that(['aHeader' => 'aHeaderVal']),
+                    'result' => 'ack',
+                    'reason' => 'aReason',
+                ]
+            )
+        ;
+
+        $consumerMock = $this->createConsumerStub(new NullQueue('aQueue'));
+        $message = new NullMessage('aBody');
+        $message->setProperty('aProp', 'aPropVal');
+        $message->setHeader('aHeader', 'aHeaderVal');
+
+        $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack('aReason'), 1, $logger);
+
+        $extension = new LogExtension();
+        $extension->onPostMessageReceived($context);
+    }
+
+    /**
+     * @return MockObject
+     */
+    private function createConsumerStub(Queue $queue): Consumer
+    {
+        $consumerMock = $this->createMock(Consumer::class);
+        $consumerMock
+            ->expects($this->any())
+            ->method('getQueue')
+            ->willReturn($queue)
+        ;
+
+        return $consumerMock;
+    }
+
+    /**
+     * @return MockObject
+     */
+    private function createContextMock(): Context
+    {
+        return $this->createMock(Context::class);
+    }
+
+    /**
+     * @return MockObject
+     */
+    private function createProcessorMock(): Processor
+    {
+        return $this->createMock(Processor::class);
+    }
+
+    /**
+     * @return MockObject|LoggerInterface
+     */
+    private function createLogger()
+    {
+        return $this->createMock(LoggerInterface::class);
+    }
+}
diff --git a/Tests/Consumption/Extension/LoggerExtensionTest.php b/Tests/Consumption/Extension/LoggerExtensionTest.php
index 0414a92..666892e 100644
--- a/Tests/Consumption/Extension/LoggerExtensionTest.php
+++ b/Tests/Consumption/Extension/LoggerExtensionTest.php
@@ -2,176 +2,78 @@
 
 namespace Enqueue\Tests\Consumption\Extension;
 
-use Enqueue\Consumption\Context;
+use Enqueue\Consumption\Context\InitLogger;
 use Enqueue\Consumption\Extension\LoggerExtension;
-use Enqueue\Consumption\ExtensionInterface;
-use Enqueue\Consumption\Result;
-use Enqueue\Null\NullMessage;
+use Enqueue\Consumption\InitLoggerExtensionInterface;
 use Enqueue\Test\ClassExtensionTrait;
-use Interop\Queue\PsrConsumer;
-use Interop\Queue\PsrContext;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
 
 class LoggerExtensionTest extends TestCase
 {
     use ClassExtensionTrait;
 
-    public function testShouldImplementExtensionInterface()
+    public function testShouldImplementInitLoggerExtensionInterface()
     {
-        $this->assertClassImplements(ExtensionInterface::class, LoggerExtension::class);
+        $this->assertClassImplements(InitLoggerExtensionInterface::class, LoggerExtension::class);
     }
 
-    public function testCouldBeConstructedWithLoggerAsFirstArgument()
-    {
-        new LoggerExtension($this->createLogger());
-    }
-
-    public function testShouldSetLoggerToContextOnStart()
+    public function testShouldSetLoggerToContextOnInitLogger()
     {
         $logger = $this->createLogger();
 
         $extension = new LoggerExtension($logger);
 
-        $context = new Context($this->createPsrContextMock());
+        $previousLogger = new NullLogger();
+        $context = new InitLogger($previousLogger);
 
-        $extension->onStart($context);
+        $extension->onInitLogger($context);
 
         $this->assertSame($logger, $context->getLogger());
     }
 
     public function testShouldAddInfoMessageOnStart()
     {
-        $logger = $this->createLogger();
-        $logger
-            ->expects($this->once())
-            ->method('debug')
-            ->with($this->stringStartsWith('Set context\'s logger'))
-        ;
-
-        $extension = new LoggerExtension($logger);
-
-        $context = new Context($this->createPsrContextMock());
-
-        $extension->onStart($context);
-    }
-
-    public function testShouldLogRejectMessageStatus()
-    {
-        $logger = $this->createLogger();
-        $logger
-            ->expects($this->once())
-            ->method('error')
-            ->with('reason', ['body' => 'message body', 'headers' => [], 'properties' => []])
-        ;
-
-        $extension = new LoggerExtension($logger);
-
-        $message = new NullMessage();
-        $message->setBody('message body');
-
-        $context = new Context($this->createPsrContextMock());
-        $context->setResult(Result::reject('reason'));
-        $context->setPsrMessage($message);
-
-        $extension->onPostReceived($context);
-    }
-
-    public function testShouldLogRequeueMessageStatus()
-    {
-        $logger = $this->createLogger();
-        $logger
-            ->expects($this->once())
-            ->method('error')
-            ->with('reason', ['body' => 'message body', 'headers' => [], 'properties' => []])
-        ;
-
-        $extension = new LoggerExtension($logger);
-
-        $message = new NullMessage();
-        $message->setBody('message body');
-
-        $context = new Context($this->createPsrContextMock());
-        $context->setResult(Result::requeue('reason'));
-        $context->setPsrMessage($message);
-
-        $extension->onPostReceived($context);
-    }
-
-    public function testShouldNotLogRequeueMessageStatusIfReasonIsEmpty()
-    {
-        $logger = $this->createLogger();
-        $logger
-            ->expects($this->never())
-            ->method('error')
-        ;
+        $previousLogger = $this->createLogger();
 
-        $extension = new LoggerExtension($logger);
-
-        $context = new Context($this->createPsrContextMock());
-        $context->setResult(Result::requeue());
-
-        $extension->onPostReceived($context);
-    }
-
-    public function testShouldLogAckMessageStatus()
-    {
         $logger = $this->createLogger();
         $logger
             ->expects($this->once())
-            ->method('info')
-            ->with('reason', ['body' => 'message body', 'headers' => [], 'properties' => []])
+            ->method('debug')
+            ->with(sprintf('Change logger from "%s" to "%s"', $logger::class, $previousLogger::class))
         ;
 
         $extension = new LoggerExtension($logger);
 
-        $message = new NullMessage();
-        $message->setBody('message body');
-
-        $context = new Context($this->createPsrContextMock());
-        $context->setResult(Result::ack('reason'));
-        $context->setPsrMessage($message);
+        $context = new InitLogger($previousLogger);
 
-        $extension->onPostReceived($context);
+        $extension->onInitLogger($context);
     }
 
-    public function testShouldNotLogAckMessageStatusIfReasonIsEmpty()
+    public function testShouldDoNothingIfSameLoggerInstanceAlreadySet()
     {
         $logger = $this->createLogger();
         $logger
             ->expects($this->never())
-            ->method('info')
+            ->method('debug')
         ;
 
         $extension = new LoggerExtension($logger);
 
-        $context = new Context($this->createPsrContextMock());
-        $context->setResult(Result::ack());
+        $context = new InitLogger($logger);
 
-        $extension->onPostReceived($context);
-    }
+        $extension->onInitLogger($context);
 
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext
-     */
-    protected function createPsrContextMock()
-    {
-        return $this->createMock(PsrContext::class);
+        $this->assertSame($logger, $context->getLogger());
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|LoggerInterface
+     * @return MockObject|LoggerInterface
      */
     protected function createLogger()
     {
         return $this->createMock(LoggerInterface::class);
     }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrConsumer
-     */
-    protected function createConsumerMock()
-    {
-        return $this->createMock(PsrConsumer::class);
-    }
 }
diff --git a/Tests/Consumption/Extension/NicenessExtensionTest.php b/Tests/Consumption/Extension/NicenessExtensionTest.php
new file mode 100644
index 0000000..734bc84
--- /dev/null
+++ b/Tests/Consumption/Extension/NicenessExtensionTest.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Enqueue\Tests\Consumption\Extension;
+
+use Enqueue\Consumption\Context\Start;
+use Enqueue\Consumption\Extension\NicenessExtension;
+use Interop\Queue\Context as InteropContext;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\NullLogger;
+
+class NicenessExtensionTest extends TestCase
+{
+    public function testShouldThrowExceptionOnInvalidArgument()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        new NicenessExtension('1');
+    }
+
+    public function testShouldThrowWarningOnInvalidArgument()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('proc_nice(): Only a super user may attempt to increase the priority of a process');
+
+        $context = new Start($this->createContextMock(), new NullLogger(), [], 0, 0);
+
+        $extension = new NicenessExtension(-1);
+        $extension->onStart($context);
+    }
+
+    /**
+     * @return MockObject|InteropContext
+     */
+    protected function createContextMock(): InteropContext
+    {
+        return $this->createMock(InteropContext::class);
+    }
+}
diff --git a/Tests/Consumption/Extension/ReplyExtensionTest.php b/Tests/Consumption/Extension/ReplyExtensionTest.php
index 4912055..cb65816 100644
--- a/Tests/Consumption/Extension/ReplyExtensionTest.php
+++ b/Tests/Consumption/Extension/ReplyExtensionTest.php
@@ -2,15 +2,17 @@
 
 namespace Enqueue\Tests\Consumption\Extension;
 
-use Enqueue\Consumption\Context;
+use Enqueue\Consumption\Context\PostMessageReceived;
 use Enqueue\Consumption\Extension\ReplyExtension;
-use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\PostMessageReceivedExtensionInterface;
 use Enqueue\Consumption\Result;
 use Enqueue\Null\NullMessage;
 use Enqueue\Null\NullQueue;
 use Enqueue\Test\ClassExtensionTrait;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrProducer;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Producer as InteropProducer;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Psr\Log\NullLogger;
 
@@ -18,52 +20,25 @@ class ReplyExtensionTest extends TestCase
 {
     use ClassExtensionTrait;
 
-    public function testShouldImplementExtensionInterface()
+    public function testShouldImplementPostMessageReceivedExtensionInterface()
     {
-        $this->assertClassImplements(ExtensionInterface::class, ReplyExtension::class);
-    }
-
-    public function testCouldBeConstructedWithoutAnyArguments()
-    {
-        new ReplyExtension();
-    }
-
-    public function testShouldDoNothingOnPreReceived()
-    {
-        $extension = new ReplyExtension();
-
-        $extension->onPreReceived(new Context($this->createNeverUsedContextMock()));
-    }
-
-    public function testShouldDoNothingOnStart()
-    {
-        $extension = new ReplyExtension();
-
-        $extension->onStart(new Context($this->createNeverUsedContextMock()));
-    }
-
-    public function testShouldDoNothingOnBeforeReceive()
-    {
-        $extension = new ReplyExtension();
-
-        $extension->onBeforeReceive(new Context($this->createNeverUsedContextMock()));
-    }
-
-    public function testShouldDoNothingOnInterrupted()
-    {
-        $extension = new ReplyExtension();
-
-        $extension->onInterrupted(new Context($this->createNeverUsedContextMock()));
+        $this->assertClassImplements(PostMessageReceivedExtensionInterface::class, ReplyExtension::class);
     }
 
     public function testShouldDoNothingIfReceivedMessageNotHaveReplyToSet()
     {
         $extension = new ReplyExtension();
 
-        $context = new Context($this->createNeverUsedContextMock());
-        $context->setPsrMessage(new NullMessage());
+        $postReceivedMessage = new PostMessageReceived(
+            $this->createNeverUsedContextMock(),
+            $this->createMock(Consumer::class),
+            new NullMessage(),
+            'aResult',
+            1,
+            new NullLogger()
+        );
 
-        $extension->onPostReceived($context);
+        $extension->onPostMessageReceived($postReceivedMessage);
     }
 
     public function testShouldDoNothingIfContextResultIsNotInstanceOfResult()
@@ -73,11 +48,16 @@ public function testShouldDoNothingIfContextResultIsNotInstanceOfResult()
         $message = new NullMessage();
         $message->setReplyTo('aReplyToQueue');
 
-        $context = new Context($this->createNeverUsedContextMock());
-        $context->setPsrMessage($message);
-        $context->setResult('notInstanceOfResult');
+        $postReceivedMessage = new PostMessageReceived(
+            $this->createNeverUsedContextMock(),
+            $this->createMock(Consumer::class),
+            $message,
+            'notInstanceOfResult',
+            1,
+            new NullLogger()
+        );
 
-        $extension->onPostReceived($context);
+        $extension->onPostMessageReceived($postReceivedMessage);
     }
 
     public function testShouldDoNothingIfResultInstanceOfResultButReplyMessageNotSet()
@@ -87,11 +67,16 @@ public function testShouldDoNothingIfResultInstanceOfResultButReplyMessageNotSet
         $message = new NullMessage();
         $message->setReplyTo('aReplyToQueue');
 
-        $context = new Context($this->createNeverUsedContextMock());
-        $context->setPsrMessage($message);
-        $context->setResult(Result::ack());
+        $postReceivedMessage = new PostMessageReceived(
+            $this->createNeverUsedContextMock(),
+            $this->createMock(Consumer::class),
+            $message,
+            Result::ack(),
+            1,
+            new NullLogger()
+        );
 
-        $extension->onPostReceived($context);
+        $extension->onPostMessageReceived($postReceivedMessage);
     }
 
     public function testShouldSendReplyMessageToReplyQueueOnPostReceived()
@@ -107,15 +92,15 @@ public function testShouldSendReplyMessageToReplyQueueOnPostReceived()
 
         $replyQueue = new NullQueue('aReplyName');
 
-        $producerMock = $this->createMock(PsrProducer::class);
+        $producerMock = $this->createMock(InteropProducer::class);
         $producerMock
             ->expects($this->once())
             ->method('send')
             ->with($replyQueue, $replyMessage)
         ;
 
-        /** @var \PHPUnit_Framework_MockObject_MockObject|PsrContext $contextMock */
-        $contextMock = $this->createMock(PsrContext::class);
+        /** @var MockObject|Context $contextMock */
+        $contextMock = $this->createMock(Context::class);
         $contextMock
             ->expects($this->once())
             ->method('createQueue')
@@ -127,20 +112,32 @@ public function testShouldSendReplyMessageToReplyQueueOnPostReceived()
             ->willReturn($producerMock)
         ;
 
-        $context = new Context($contextMock);
-        $context->setPsrMessage($message);
-        $context->setResult(Result::reply($replyMessage));
-        $context->setLogger(new NullLogger());
+        $postReceivedMessage = new PostMessageReceived(
+            $contextMock,
+            $this->createMock(Consumer::class),
+            $message,
+            Result::reply($replyMessage),
+            1,
+            new NullLogger()
+        );
+
+        $extension->onPostMessageReceived($postReceivedMessage);
+    }
 
-        $extension->onPostReceived($context);
+    /**
+     * @return MockObject
+     */
+    protected function createInteropContextMock(): Context
+    {
+        return $this->createMock(Context::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext
+     * @return MockObject
      */
-    private function createNeverUsedContextMock()
+    private function createNeverUsedContextMock(): Context
     {
-        $contextMock = $this->createMock(PsrContext::class);
+        $contextMock = $this->createMock(Context::class);
         $contextMock
             ->expects($this->never())
             ->method('createProducer')
diff --git a/Tests/Consumption/FallbackSubscriptionConsumerTest.php b/Tests/Consumption/FallbackSubscriptionConsumerTest.php
new file mode 100644
index 0000000..73fba7b
--- /dev/null
+++ b/Tests/Consumption/FallbackSubscriptionConsumerTest.php
@@ -0,0 +1,253 @@
+<?php
+
+namespace Enqueue\Tests\Consumption;
+
+use Enqueue\Consumption\FallbackSubscriptionConsumer;
+use Enqueue\Test\ReadAttributeTrait;
+use Interop\Queue\Consumer;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Queue as InteropQueue;
+use Interop\Queue\SubscriptionConsumer;
+use PHPUnit\Framework\TestCase;
+
+class FallbackSubscriptionConsumerTest extends TestCase
+{
+    use ReadAttributeTrait;
+
+    public function testShouldImplementSubscriptionConsumerInterface()
+    {
+        $rc = new \ReflectionClass(FallbackSubscriptionConsumer::class);
+
+        $this->assertTrue($rc->implementsInterface(SubscriptionConsumer::class));
+    }
+
+    public function testShouldInitSubscribersPropertyWithEmptyArray()
+    {
+        $subscriptionConsumer = new FallbackSubscriptionConsumer();
+
+        $this->assertAttributeSame([], 'subscribers', $subscriptionConsumer);
+    }
+
+    public function testShouldAddConsumerAndCallbackToSubscribersPropertyOnSubscribe()
+    {
+        $subscriptionConsumer = new FallbackSubscriptionConsumer();
+
+        $fooCallback = function () {};
+        $fooConsumer = $this->createConsumerStub('foo_queue');
+
+        $barCallback = function () {};
+        $barConsumer = $this->createConsumerStub('bar_queue');
+
+        $subscriptionConsumer->subscribe($fooConsumer, $fooCallback);
+        $subscriptionConsumer->subscribe($barConsumer, $barCallback);
+
+        $this->assertAttributeSame([
+            'foo_queue' => [$fooConsumer, $fooCallback],
+            'bar_queue' => [$barConsumer, $barCallback],
+        ], 'subscribers', $subscriptionConsumer);
+    }
+
+    public function testThrowsIfTrySubscribeAnotherConsumerToAlreadySubscribedQueue()
+    {
+        $subscriptionConsumer = new FallbackSubscriptionConsumer();
+
+        $fooCallback = function () {};
+        $fooConsumer = $this->createConsumerStub('foo_queue');
+
+        $barCallback = function () {};
+        $barConsumer = $this->createConsumerStub('foo_queue');
+
+        $subscriptionConsumer->subscribe($fooConsumer, $fooCallback);
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('There is a consumer subscribed to queue: "foo_queue"');
+        $subscriptionConsumer->subscribe($barConsumer, $barCallback);
+    }
+
+    /**
+     * @doesNotPerformAssertions
+     */
+    public function testShouldAllowSubscribeSameConsumerAndCallbackSecondTime()
+    {
+        $subscriptionConsumer = new FallbackSubscriptionConsumer();
+
+        $fooCallback = function () {};
+        $fooConsumer = $this->createConsumerStub('foo_queue');
+
+        $subscriptionConsumer->subscribe($fooConsumer, $fooCallback);
+        $subscriptionConsumer->subscribe($fooConsumer, $fooCallback);
+    }
+
+    public function testShouldRemoveSubscribedConsumerOnUnsubscribeCall()
+    {
+        $subscriptionConsumer = new FallbackSubscriptionConsumer();
+
+        $fooConsumer = $this->createConsumerStub('foo_queue');
+        $barConsumer = $this->createConsumerStub('bar_queue');
+
+        $subscriptionConsumer->subscribe($fooConsumer, function () {});
+        $subscriptionConsumer->subscribe($barConsumer, function () {});
+
+        // guard
+        $this->assertAttributeCount(2, 'subscribers', $subscriptionConsumer);
+
+        $subscriptionConsumer->unsubscribe($fooConsumer);
+
+        $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer);
+    }
+
+    public function testShouldDoNothingIfTryUnsubscribeNotSubscribedQueueName()
+    {
+        $subscriptionConsumer = new FallbackSubscriptionConsumer();
+
+        $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {});
+
+        // guard
+        $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer);
+
+        $subscriptionConsumer->unsubscribe($this->createConsumerStub('bar_queue'));
+
+        $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer);
+    }
+
+    public function testShouldDoNothingIfTryUnsubscribeNotSubscribedConsumer()
+    {
+        $subscriptionConsumer = new FallbackSubscriptionConsumer();
+
+        $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {});
+
+        // guard
+        $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer);
+
+        $subscriptionConsumer->unsubscribe($this->createConsumerStub('foo_queue'));
+
+        $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer);
+    }
+
+    public function testShouldRemoveAllSubscriberOnUnsubscribeAllCall()
+    {
+        $subscriptionConsumer = new FallbackSubscriptionConsumer();
+
+        $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {});
+        $subscriptionConsumer->subscribe($this->createConsumerStub('bar_queue'), function () {});
+
+        // guard
+        $this->assertAttributeCount(2, 'subscribers', $subscriptionConsumer);
+
+        $subscriptionConsumer->unsubscribeAll();
+
+        $this->assertAttributeCount(0, 'subscribers', $subscriptionConsumer);
+    }
+
+    public function testShouldConsumeMessagesFromTwoQueuesInExpectedOrder()
+    {
+        $firstMessage = $this->createMessageStub('first');
+        $secondMessage = $this->createMessageStub('second');
+        $thirdMessage = $this->createMessageStub('third');
+        $fourthMessage = $this->createMessageStub('fourth');
+        $fifthMessage = $this->createMessageStub('fifth');
+
+        $fooConsumer = $this->createConsumerStub('foo_queue');
+        $fooConsumer
+            ->expects($this->any())
+            ->method('receiveNoWait')
+            ->willReturnOnConsecutiveCalls(null, $firstMessage, null, $secondMessage, $thirdMessage)
+        ;
+
+        $barConsumer = $this->createConsumerStub('bar_queue');
+        $barConsumer
+            ->expects($this->any())
+            ->method('receiveNoWait')
+            ->willReturnOnConsecutiveCalls($fourthMessage, null, null, $fifthMessage)
+        ;
+
+        $actualOrder = [];
+        $callback = function (InteropMessage $message, Consumer $consumer) use (&$actualOrder) {
+            $actualOrder[] = [$message->getBody(), $consumer->getQueue()->getQueueName()];
+        };
+
+        $subscriptionConsumer = new FallbackSubscriptionConsumer();
+
+        $subscriptionConsumer->subscribe($fooConsumer, $callback);
+        $subscriptionConsumer->subscribe($barConsumer, $callback);
+
+        $subscriptionConsumer->consume(100);
+
+        $this->assertEquals([
+            ['fourth', 'bar_queue'],
+            ['first', 'foo_queue'],
+            ['second', 'foo_queue'],
+            ['fifth', 'bar_queue'],
+            ['third', 'foo_queue'],
+        ], $actualOrder);
+    }
+
+    public function testThrowsIfTryConsumeWithoutSubscribers()
+    {
+        $subscriptionConsumer = new FallbackSubscriptionConsumer();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('No subscribers');
+        $subscriptionConsumer->consume();
+    }
+
+    public function testShouldConsumeTillTimeoutIsReached()
+    {
+        $fooConsumer = $this->createConsumerStub('foo_queue');
+        $fooConsumer
+            ->expects($this->any())
+            ->method('receiveNoWait')
+            ->willReturn(null)
+        ;
+
+        $subscriptionConsumer = new FallbackSubscriptionConsumer();
+
+        $subscriptionConsumer->subscribe($fooConsumer, function () {});
+
+        $startAt = microtime(true);
+        $subscriptionConsumer->consume(500);
+        $endAt = microtime(true);
+
+        $this->assertGreaterThan(0.49, $endAt - $startAt);
+    }
+
+    /**
+     * @param mixed|null $body
+     *
+     * @return InteropMessage|\PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createMessageStub($body = null)
+    {
+        $messageMock = $this->createMock(InteropMessage::class);
+        $messageMock
+            ->expects($this->any())
+            ->method('getBody')
+            ->willReturn($body)
+        ;
+
+        return $messageMock;
+    }
+
+    /**
+     * @param mixed|null $queueName
+     *
+     * @return Consumer|\PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createConsumerStub($queueName = null)
+    {
+        $queueMock = $this->createMock(InteropQueue::class);
+        $queueMock
+            ->expects($this->any())
+            ->method('getQueueName')
+            ->willReturn($queueName);
+
+        $consumerMock = $this->createMock(Consumer::class);
+        $consumerMock
+            ->expects($this->any())
+            ->method('getQueue')
+            ->willReturn($queueMock)
+        ;
+
+        return $consumerMock;
+    }
+}
diff --git a/Tests/Consumption/Mock/BreakCycleExtension.php b/Tests/Consumption/Mock/BreakCycleExtension.php
index b61d8dd..cbc2f8b 100644
--- a/Tests/Consumption/Mock/BreakCycleExtension.php
+++ b/Tests/Consumption/Mock/BreakCycleExtension.php
@@ -2,14 +2,20 @@
 
 namespace Enqueue\Tests\Consumption\Mock;
 
-use Enqueue\Consumption\Context;
-use Enqueue\Consumption\EmptyExtensionTrait;
+use Enqueue\Consumption\Context\End;
+use Enqueue\Consumption\Context\InitLogger;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\Context\MessageResult;
+use Enqueue\Consumption\Context\PostConsume;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\PreConsume;
+use Enqueue\Consumption\Context\PreSubscribe;
+use Enqueue\Consumption\Context\ProcessorException;
+use Enqueue\Consumption\Context\Start;
 use Enqueue\Consumption\ExtensionInterface;
 
 class BreakCycleExtension implements ExtensionInterface
 {
-    use EmptyExtensionTrait;
-
     protected $cycles = 1;
 
     private $limit;
@@ -19,15 +25,51 @@ public function __construct($limit)
         $this->limit = $limit;
     }
 
-    public function onPostReceived(Context $context)
+    public function onInitLogger(InitLogger $context): void
+    {
+    }
+
+    public function onPostMessageReceived(PostMessageReceived $context): void
+    {
+        if ($this->cycles >= $this->limit) {
+            $context->interruptExecution();
+        } else {
+            ++$this->cycles;
+        }
+    }
+
+    public function onEnd(End $context): void
+    {
+    }
+
+    public function onMessageReceived(MessageReceived $context): void
+    {
+    }
+
+    public function onResult(MessageResult $context): void
+    {
+    }
+
+    public function onPreConsume(PreConsume $context): void
+    {
+    }
+
+    public function onPreSubscribe(PreSubscribe $context): void
+    {
+    }
+
+    public function onProcessorException(ProcessorException $context): void
+    {
+    }
+
+    public function onStart(Start $context): void
     {
-        $this->onIdle($context);
     }
 
-    public function onIdle(Context $context)
+    public function onPostConsume(PostConsume $context): void
     {
         if ($this->cycles >= $this->limit) {
-            $context->setExecutionInterrupted(true);
+            $context->interruptExecution();
         } else {
             ++$this->cycles;
         }
diff --git a/Tests/Consumption/Mock/DummySubscriptionConsumer.php b/Tests/Consumption/Mock/DummySubscriptionConsumer.php
new file mode 100644
index 0000000..4035148
--- /dev/null
+++ b/Tests/Consumption/Mock/DummySubscriptionConsumer.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Enqueue\Tests\Consumption\Mock;
+
+use Interop\Queue\Consumer;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\SubscriptionConsumer;
+
+class DummySubscriptionConsumer implements SubscriptionConsumer
+{
+    private $subscriptions = [];
+
+    private $messages = [];
+
+    /**
+     * @param float|int $timeout milliseconds 1000 is 1 second, a zero is consume endlessly
+     */
+    public function consume(int $timeout = 0): void
+    {
+        foreach ($this->messages as list($message, $queueName)) {
+            /** @var InteropMessage $message */
+            /** @var string $queueName */
+            if (false == call_user_func($this->subscriptions[$queueName][1], $message, $this->subscriptions[$queueName][0])) {
+                return;
+            }
+        }
+    }
+
+    public function subscribe(Consumer $consumer, callable $callback): void
+    {
+        $this->subscriptions[$consumer->getQueue()->getQueueName()] = [$consumer, $callback];
+    }
+
+    public function unsubscribe(Consumer $consumer): void
+    {
+        unset($this->subscriptions[$consumer->getQueue()->getQueueName()]);
+    }
+
+    public function unsubscribeAll(): void
+    {
+        $this->subscriptions = [];
+    }
+
+    public function addMessage(InteropMessage $message, string $queueName): void
+    {
+        $this->messages[] = [$message, $queueName];
+    }
+}
diff --git a/Tests/Consumption/QueueConsumerTest.php b/Tests/Consumption/QueueConsumerTest.php
index 196f755..2bcc253 100644
--- a/Tests/Consumption/QueueConsumerTest.php
+++ b/Tests/Consumption/QueueConsumerTest.php
@@ -2,61 +2,92 @@
 
 namespace Enqueue\Tests\Consumption;
 
+use Enqueue\Consumption\BoundProcessor;
 use Enqueue\Consumption\CallbackProcessor;
 use Enqueue\Consumption\ChainExtension;
-use Enqueue\Consumption\Context;
+use Enqueue\Consumption\Context\End;
+use Enqueue\Consumption\Context\InitLogger;
+use Enqueue\Consumption\Context\MessageReceived;
+use Enqueue\Consumption\Context\MessageResult;
+use Enqueue\Consumption\Context\PostConsume;
+use Enqueue\Consumption\Context\PostMessageReceived;
+use Enqueue\Consumption\Context\PreConsume;
+use Enqueue\Consumption\Context\PreSubscribe;
+use Enqueue\Consumption\Context\ProcessorException;
+use Enqueue\Consumption\Context\Start;
 use Enqueue\Consumption\Exception\InvalidArgumentException;
+use Enqueue\Consumption\Extension\ExitStatusExtension;
 use Enqueue\Consumption\ExtensionInterface;
 use Enqueue\Consumption\QueueConsumer;
 use Enqueue\Consumption\Result;
 use Enqueue\Null\NullQueue;
+use Enqueue\Test\ReadAttributeTrait;
 use Enqueue\Tests\Consumption\Mock\BreakCycleExtension;
-use Interop\Queue\PsrConsumer;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrMessage;
-use Interop\Queue\PsrProcessor;
-use Interop\Queue\PsrQueue;
+use Enqueue\Tests\Consumption\Mock\DummySubscriptionConsumer;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context as InteropContext;
+use Interop\Queue\Exception\SubscriptionConsumerNotSupportedException;
+use Interop\Queue\Message;
+use Interop\Queue\Processor;
+use Interop\Queue\Queue;
+use Interop\Queue\SubscriptionConsumer;
 use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
 use Psr\Log\NullLogger;
 
 class QueueConsumerTest extends TestCase
 {
-    public function testCouldBeConstructedWithConnectionAndExtensionsAsArguments()
+    use ReadAttributeTrait;
+
+    public function testShouldSetEmptyArrayToBoundProcessorsPropertyInConstructor()
     {
-        new QueueConsumer($this->createPsrContextStub(), null, 0);
+        $consumer = new QueueConsumer($this->createContextStub(), null, [], null, 0);
+
+        $this->assertAttributeSame([], 'boundProcessors', $consumer);
     }
 
-    public function testCouldBeConstructedWithConnectionOnly()
+    public function testShouldSetProvidedBoundProcessorsToThePropertyInConstructor()
     {
-        new QueueConsumer($this->createPsrContextStub());
+        $boundProcessors = [
+            new BoundProcessor(new NullQueue('foo'), $this->createProcessorMock()),
+            new BoundProcessor(new NullQueue('bar'), $this->createProcessorMock()),
+        ];
+
+        $consumer = new QueueConsumer($this->createContextStub(), null, $boundProcessors, null, 0);
+
+        $this->assertAttributeSame($boundProcessors, 'boundProcessors', $consumer);
     }
 
-    public function testCouldBeConstructedWithConnectionAndSingleExtension()
+    public function testShouldSetNullLoggerIfNoneProvidedInConstructor()
     {
-        new QueueConsumer($this->createPsrContextStub(), $this->createExtension());
+        $consumer = new QueueConsumer($this->createContextStub(), null, [], null, 0);
+
+        $this->assertAttributeInstanceOf(NullLogger::class, 'logger', $consumer);
     }
 
-    public function testShouldSetEmptyArrayToBoundProcessorsPropertyInConstructor()
+    public function testShouldSetProvidedLoggerToThePropertyInConstructor()
     {
-        $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0);
+        $expectedLogger = $this->createMock(LoggerInterface::class);
 
-        $this->assertAttributeSame([], 'boundProcessors', $consumer);
+        $consumer = new QueueConsumer($this->createContextStub(), null, [], $expectedLogger, 0);
+
+        $this->assertAttributeSame($expectedLogger, 'logger', $consumer);
     }
 
-    public function testShouldAllowGetConnectionSetInConstructor()
+    public function testShouldAllowGetContextSetInConstructor()
     {
-        $expectedConnection = $this->createPsrContextStub();
+        $expectedContext = $this->createContextStub();
 
-        $consumer = new QueueConsumer($expectedConnection, null, 0);
+        $consumer = new QueueConsumer($expectedContext, null, [], null, 0);
 
-        $this->assertSame($expectedConnection, $consumer->getPsrContext());
+        $this->assertSame($expectedContext, $consumer->getContext());
     }
 
     public function testThrowIfQueueNameEmptyOnBind()
     {
         $processorMock = $this->createProcessorMock();
 
-        $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0);
+        $consumer = new QueueConsumer($this->createContextStub());
 
         $this->expectException(\LogicException::class);
         $this->expectExceptionMessage('The queue name must be not empty.');
@@ -67,7 +98,7 @@ public function testThrowIfQueueAlreadyBoundToProcessorOnBind()
     {
         $processorMock = $this->createProcessorMock();
 
-        $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0);
+        $consumer = new QueueConsumer($this->createContextStub());
 
         $consumer->bind(new NullQueue('theQueueName'), $processorMock);
 
@@ -81,45 +112,31 @@ public function testShouldAllowBindProcessorToQueue()
         $queue = new NullQueue('theQueueName');
         $processorMock = $this->createProcessorMock();
 
-        $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0);
+        $consumer = new QueueConsumer($this->createContextStub());
 
         $consumer->bind($queue, $processorMock);
 
-        $this->assertAttributeSame(['theQueueName' => [$queue, $processorMock]], 'boundProcessors', $consumer);
+        $this->assertAttributeEquals(
+            ['theQueueName' => new BoundProcessor($queue, $processorMock)],
+            'boundProcessors',
+            $consumer
+        );
     }
 
     public function testThrowIfQueueNeitherInstanceOfQueueNorString()
     {
         $processorMock = $this->createProcessorMock();
 
-        $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0);
+        $consumer = new QueueConsumer($this->createContextStub());
 
         $this->expectException(InvalidArgumentException::class);
-        $this->expectExceptionMessage('The argument must be an instance of Interop\Queue\PsrQueue but got stdClass.');
+        $this->expectExceptionMessage('The argument must be an instance of Interop\Queue\Queue but got stdClass.');
         $consumer->bind(new \stdClass(), $processorMock);
     }
 
-    public function testThrowIfProcessorNeitherInstanceOfProcessorNorCallable()
-    {
-        $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0);
-
-        $this->expectException(InvalidArgumentException::class);
-        $this->expectExceptionMessage('The argument must be an instance of Interop\Queue\PsrProcessor but got stdClass.');
-        $consumer->bind(new NullQueue(''), new \stdClass());
-    }
-
-    public function testCouldSetGetIdleTimeout()
-    {
-        $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0);
-
-        $consumer->setIdleTimeout(123456);
-
-        $this->assertSame(123456, $consumer->getIdleTimeout());
-    }
-
     public function testCouldSetGetReceiveTimeout()
     {
-        $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0);
+        $consumer = new QueueConsumer($this->createContextStub());
 
         $consumer->setReceiveTimeout(123456);
 
@@ -134,7 +151,7 @@ public function testShouldAllowBindCallbackToQueueName()
         $queueName = 'theQueueName';
         $queue = new NullQueue($queueName);
 
-        $context = $this->createMock(PsrContext::class);
+        $context = $this->createContextWithoutSubscriptionConsumerMock();
         $context
             ->expects($this->once())
             ->method('createQueue')
@@ -142,49 +159,129 @@ public function testShouldAllowBindCallbackToQueueName()
             ->willReturn($queue)
         ;
 
-        $consumer = new QueueConsumer($context, null, 0);
+        $consumer = new QueueConsumer($context);
 
-        $consumer->bind($queueName, $callback);
+        $consumer->bindCallback($queueName, $callback);
 
         $boundProcessors = $this->readAttribute($consumer, 'boundProcessors');
 
-        $this->assertInternalType('array', $boundProcessors);
+        self::assertIsArray($boundProcessors);
         $this->assertCount(1, $boundProcessors);
         $this->assertArrayHasKey($queueName, $boundProcessors);
 
-        $this->assertInternalType('array', $boundProcessors[$queueName]);
-        $this->assertCount(2, $boundProcessors[$queueName]);
-        $this->assertSame($queue, $boundProcessors[$queueName][0]);
-        $this->assertInstanceOf(CallbackProcessor::class, $boundProcessors[$queueName][1]);
+        $this->assertInstanceOf(BoundProcessor::class, $boundProcessors[$queueName]);
+        $this->assertSame($queue, $boundProcessors[$queueName]->getQueue());
+        $this->assertInstanceOf(CallbackProcessor::class, $boundProcessors[$queueName]->getProcessor());
     }
 
     public function testShouldReturnSelfOnBind()
     {
         $processorMock = $this->createProcessorMock();
 
-        $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0);
+        $consumer = new QueueConsumer($this->createContextStub());
 
-        $this->assertSame($consumer, $consumer->bind(new NullQueue('aQueueName'), $processorMock));
+        $this->assertSame($consumer, $consumer->bind(new NullQueue('foo_queue'), $processorMock));
+    }
+
+    public function testShouldUseContextSubscriptionConsumerIfSupport()
+    {
+        $expectedQueue = new NullQueue('theQueueName');
+
+        $contextSubscriptionConsumer = $this->createSubscriptionConsumerMock();
+        $contextSubscriptionConsumer
+            ->expects($this->once())
+            ->method('consume')
+        ;
+
+        $fallbackSubscriptionConsumer = $this->createSubscriptionConsumerMock();
+        $fallbackSubscriptionConsumer
+            ->expects($this->never())
+            ->method('consume')
+        ;
+
+        $contextMock = $this->createMock(InteropContext::class);
+        $contextMock
+            ->expects($this->once())
+            ->method('createConsumer')
+            ->with($this->identicalTo($expectedQueue))
+            ->willReturn($this->createConsumerStub())
+        ;
+        $contextMock
+            ->expects($this->once())
+            ->method('createSubscriptionConsumer')
+            ->willReturn($contextSubscriptionConsumer)
+        ;
+
+        $processorMock = $this->createProcessorMock();
+        $processorMock
+            ->expects($this->never())
+            ->method('process')
+        ;
+
+        $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(1));
+        $queueConsumer->setFallbackSubscriptionConsumer($fallbackSubscriptionConsumer);
+        $queueConsumer->bind($expectedQueue, $processorMock);
+        $queueConsumer->consume();
+    }
+
+    public function testShouldUseFallbackSubscriptionConsumerIfNotSupported()
+    {
+        $expectedQueue = new NullQueue('theQueueName');
+
+        $contextSubscriptionConsumer = $this->createSubscriptionConsumerMock();
+        $contextSubscriptionConsumer
+            ->expects($this->never())
+            ->method('consume')
+        ;
+
+        $fallbackSubscriptionConsumer = $this->createSubscriptionConsumerMock();
+        $fallbackSubscriptionConsumer
+            ->expects($this->once())
+            ->method('consume')
+        ;
+
+        $contextMock = $this->createContextWithoutSubscriptionConsumerMock();
+        $contextMock
+            ->expects($this->once())
+            ->method('createConsumer')
+            ->with($this->identicalTo($expectedQueue))
+            ->willReturn($this->createConsumerStub())
+        ;
+        $contextMock
+            ->expects($this->once())
+            ->method('createSubscriptionConsumer')
+            ->willThrowException(SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt())
+        ;
+
+        $processorMock = $this->createProcessorMock();
+        $processorMock
+            ->expects($this->never())
+            ->method('process')
+        ;
+
+        $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(1));
+        $queueConsumer->setFallbackSubscriptionConsumer($fallbackSubscriptionConsumer);
+        $queueConsumer->bind($expectedQueue, $processorMock);
+        $queueConsumer->consume();
     }
 
     public function testShouldSubscribeToGivenQueueWithExpectedTimeout()
     {
         $expectedQueue = new NullQueue('theQueueName');
 
-        $messageConsumerMock = $this->createMock(PsrConsumer::class);
-        $messageConsumerMock
+        $subscriptionConsumerMock = $this->createSubscriptionConsumerMock();
+        $subscriptionConsumerMock
             ->expects($this->once())
-            ->method('receive')
+            ->method('consume')
             ->with(12345)
-            ->willReturn(null)
         ;
 
-        $contextMock = $this->createMock(PsrContext::class);
+        $contextMock = $this->createContextWithoutSubscriptionConsumerMock();
         $contextMock
             ->expects($this->once())
             ->method('createConsumer')
             ->with($this->identicalTo($expectedQueue))
-            ->willReturn($messageConsumerMock)
+            ->willReturn($this->createConsumerStub())
         ;
 
         $processorMock = $this->createProcessorMock();
@@ -193,28 +290,28 @@ public function testShouldSubscribeToGivenQueueWithExpectedTimeout()
             ->method('process')
         ;
 
-        $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(1), 0, 12345);
+        $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(1), [], null, 12345);
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
         $queueConsumer->bind($expectedQueue, $processorMock);
         $queueConsumer->consume();
     }
 
-    public function testShouldSubscribeToGivenQueueAndQuitAfterFifthIdleCycle()
+    public function testShouldSubscribeToGivenQueueAndQuitAfterFifthConsumeCycle()
     {
         $expectedQueue = new NullQueue('theQueueName');
 
-        $messageConsumerMock = $this->createMock(PsrConsumer::class);
-        $messageConsumerMock
+        $subscriptionConsumerMock = $this->createSubscriptionConsumerMock();
+        $subscriptionConsumerMock
             ->expects($this->exactly(5))
-            ->method('receive')
-            ->willReturn(null)
+            ->method('consume')
         ;
 
-        $contextMock = $this->createMock(PsrContext::class);
+        $contextMock = $this->createContextWithoutSubscriptionConsumerMock();
         $contextMock
             ->expects($this->once())
             ->method('createConsumer')
             ->with($this->identicalTo($expectedQueue))
-            ->willReturn($messageConsumerMock)
+            ->willReturn($this->createConsumerStub())
         ;
 
         $processorMock = $this->createProcessorMock();
@@ -223,17 +320,30 @@ public function testShouldSubscribeToGivenQueueAndQuitAfterFifthIdleCycle()
             ->method('process')
         ;
 
-        $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(5), 0);
+        $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(5));
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
         $queueConsumer->bind($expectedQueue, $processorMock);
         $queueConsumer->consume();
     }
 
     public function testShouldProcessFiveMessagesAndQuit()
     {
-        $messageMock = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($messageMock);
+        $fooQueue = new NullQueue('foo_queue');
+
+        $firstMessageMock = $this->createMessageMock();
+        $secondMessageMock = $this->createMessageMock();
+        $thirdMessageMock = $this->createMessageMock();
+        $fourthMessageMock = $this->createMessageMock();
+        $fifthMessageMock = $this->createMessageMock();
+
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($firstMessageMock, 'foo_queue');
+        $subscriptionConsumerMock->addMessage($secondMessageMock, 'foo_queue');
+        $subscriptionConsumerMock->addMessage($thirdMessageMock, 'foo_queue');
+        $subscriptionConsumerMock->addMessage($fourthMessageMock, 'foo_queue');
+        $subscriptionConsumerMock->addMessage($fifthMessageMock, 'foo_queue');
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $contextStub = $this->createContextStub();
 
         $processorMock = $this->createProcessorMock();
         $processorMock
@@ -242,8 +352,9 @@ public function testShouldProcessFiveMessagesAndQuit()
             ->willReturn(Result::ACK)
         ;
 
-        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(5), 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(5));
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind($fooQueue, $processorMock);
 
         $queueConsumer->consume();
     }
@@ -251,14 +362,18 @@ public function testShouldProcessFiveMessagesAndQuit()
     public function testShouldAckMessageIfProcessorReturnSuchStatus()
     {
         $messageMock = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($messageMock);
-        $messageConsumerStub
+
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+        $consumerStub
             ->expects($this->once())
             ->method('acknowledge')
             ->with($this->identicalTo($messageMock))
         ;
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
         $processorMock
@@ -268,8 +383,9 @@ public function testShouldAckMessageIfProcessorReturnSuchStatus()
             ->willReturn(Result::ACK)
         ;
 
-        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1));
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
@@ -277,9 +393,13 @@ public function testShouldAckMessageIfProcessorReturnSuchStatus()
     public function testThrowIfProcessorReturnNull()
     {
         $messageMock = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($messageMock);
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
         $processorMock
@@ -289,8 +409,9 @@ public function testThrowIfProcessorReturnNull()
             ->willReturn(null)
         ;
 
-        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1));
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $this->expectException(\LogicException::class);
         $this->expectExceptionMessage('Status is not supported');
@@ -300,14 +421,18 @@ public function testThrowIfProcessorReturnNull()
     public function testShouldRejectMessageIfProcessorReturnSuchStatus()
     {
         $messageMock = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($messageMock);
-        $messageConsumerStub
+
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+        $consumerStub
             ->expects($this->once())
             ->method('reject')
             ->with($this->identicalTo($messageMock), false)
         ;
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
         $processorMock
@@ -317,8 +442,43 @@ public function testShouldRejectMessageIfProcessorReturnSuchStatus()
             ->willReturn(Result::REJECT)
         ;
 
-        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1));
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
+
+        $queueConsumer->consume();
+    }
+
+    public function testShouldDoNothingIfProcessorReturnsAlreadyAcknowledged()
+    {
+        $messageMock = $this->createMessageMock();
+
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+        $consumerStub
+            ->expects($this->never())
+            ->method('reject')
+        ;
+        $consumerStub
+            ->expects($this->never())
+            ->method('acknowledge')
+        ;
+
+        $contextStub = $this->createContextStub($consumerStub);
+
+        $processorMock = $this->createProcessorMock();
+        $processorMock
+            ->expects($this->once())
+            ->method('process')
+            ->with($this->identicalTo($messageMock))
+            ->willReturn(Result::ALREADY_ACKNOWLEDGED)
+        ;
+
+        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1));
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
@@ -326,14 +486,18 @@ public function testShouldRejectMessageIfProcessorReturnSuchStatus()
     public function testShouldRequeueMessageIfProcessorReturnSuchStatus()
     {
         $messageMock = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($messageMock);
-        $messageConsumerStub
+
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+        $consumerStub
             ->expects($this->once())
             ->method('reject')
             ->with($this->identicalTo($messageMock), true)
         ;
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
         $processorMock
@@ -343,8 +507,9 @@ public function testShouldRequeueMessageIfProcessorReturnSuchStatus()
             ->willReturn(Result::REQUEUE)
         ;
 
-        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1));
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
@@ -352,9 +517,13 @@ public function testShouldRequeueMessageIfProcessorReturnSuchStatus()
     public function testThrowIfProcessorReturnInvalidStatus()
     {
         $messageMock = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($messageMock);
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
         $processorMock
@@ -364,8 +533,9 @@ public function testThrowIfProcessorReturnInvalidStatus()
             ->willReturn('invalidStatus')
         ;
 
-        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1));
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $this->expectException(\LogicException::class);
         $this->expectExceptionMessage('Status is not supported: invalidStatus');
@@ -377,17 +547,21 @@ public function testShouldNotPassMessageToProcessorIfItWasProcessedByExtension()
         $extension = $this->createExtension();
         $extension
             ->expects($this->once())
-            ->method('onPreReceived')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) {
-                $context->setResult(Result::ACK);
+            ->method('onMessageReceived')
+            ->with($this->isInstanceOf(MessageReceived::class))
+            ->willReturnCallback(function (MessageReceived $context) {
+                $context->setResult(Result::ack());
             })
         ;
 
         $messageMock = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($messageMock);
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
         $processorMock
@@ -396,17 +570,46 @@ public function testShouldNotPassMessageToProcessorIfItWasProcessedByExtension()
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
+
+        $queueConsumer->consume();
+    }
+
+    public function testShouldCallOnInitLoggerExtensionMethod()
+    {
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
+
+        $processorMock = $this->createProcessorMock();
+
+        $logger = $this->createMock(LoggerInterface::class);
+
+        $extension = $this->createExtension();
+        $extension
+            ->expects($this->once())
+            ->method('onInitLogger')
+            ->with($this->isInstanceOf(InitLogger::class))
+            ->willReturnCallback(function (InitLogger $context) use ($logger) {
+                $this->assertSame($logger, $context->getLogger());
+            })
+        ;
+
+        $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, [], $logger);
+        $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer());
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
 
     public function testShouldCallOnStartExtensionMethod()
     {
-        $messageConsumerStub = $this->createMessageConsumerStub($message = null);
+        $consumerStub = $this->createConsumerStub('foo_queue');
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
 
@@ -414,472 +617,530 @@ public function testShouldCallOnStartExtensionMethod()
         $extension
             ->expects($this->once())
             ->method('onStart')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use ($contextStub) {
-                $this->assertSame($contextStub, $context->getPsrContext());
-                $this->assertNull($context->getPsrConsumer());
-                $this->assertNull($context->getPsrProcessor());
-                $this->assertNull($context->getLogger());
-                $this->assertNull($context->getPsrMessage());
-                $this->assertNull($context->getException());
-                $this->assertNull($context->getResult());
-                $this->assertNull($context->getPsrQueue());
-                $this->assertFalse($context->isExecutionInterrupted());
+            ->with($this->isInstanceOf(Start::class))
+            ->willReturnCallback(function (Start $context) use ($contextStub) {
+                $this->assertSame($contextStub, $context->getContext());
+                $this->assertInstanceOf(NullLogger::class, $context->getLogger());
             })
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer());
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
 
-    public function testShouldCallOnIdleExtensionMethod()
+    public function testShouldCallOnStartWithLoggerProvidedInConstructor()
     {
-        $messageConsumerStub = $this->createMessageConsumerStub($message = null);
+        $consumerStub = $this->createConsumerStub('foo_queue');
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
 
+        $expectedLogger = $this->createMock(LoggerInterface::class);
+
         $extension = $this->createExtension();
         $extension
             ->expects($this->once())
-            ->method('onIdle')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use (
-                $contextStub,
-                $messageConsumerStub,
-                $processorMock
-            ) {
-                $this->assertSame($contextStub, $context->getPsrContext());
-                $this->assertSame($messageConsumerStub, $context->getPsrConsumer());
-                $this->assertSame($processorMock, $context->getPsrProcessor());
-                $this->assertInstanceOf(NullLogger::class, $context->getLogger());
-                $this->assertNull($context->getPsrMessage());
-                $this->assertNull($context->getException());
-                $this->assertNull($context->getResult());
-                $this->assertFalse($context->isExecutionInterrupted());
+            ->method('onStart')
+            ->with($this->isInstanceOf(Start::class))
+            ->willReturnCallback(function (Start $context) use ($expectedLogger) {
+                $this->assertSame($expectedLogger, $context->getLogger());
             })
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, [], $expectedLogger);
+        $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer());
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
 
-    public function testShouldCallOnBeforeReceiveExtensionMethod()
+    public function testShouldInterruptExecutionOnStart()
     {
-        $expectedMessage = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage);
+        $consumerStub = $this->createConsumerStub('foo_queue');
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $contextStub = $this->createContextStub($consumerStub);
 
-        $processorMock = $this->createProcessorStub();
+        $processorMock = $this->createProcessorMock();
+        $processorMock
+            ->expects($this->never())
+            ->method('process')
+        ;
 
-        $queue = new NullQueue('aQueueName');
+        $expectedLogger = $this->createMock(LoggerInterface::class);
 
         $extension = $this->createExtension();
         $extension
             ->expects($this->once())
-            ->method('onBeforeReceive')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use (
-                $contextStub,
-                $messageConsumerStub,
-                $processorMock,
-                $queue
-            ) {
-                $this->assertSame($contextStub, $context->getPsrContext());
-                $this->assertSame($messageConsumerStub, $context->getPsrConsumer());
-                $this->assertSame($processorMock, $context->getPsrProcessor());
-                $this->assertInstanceOf(NullLogger::class, $context->getLogger());
-                $this->assertNull($context->getPsrMessage());
-                $this->assertNull($context->getException());
-                $this->assertNull($context->getResult());
-                $this->assertFalse($context->isExecutionInterrupted());
-                $this->assertSame($queue, $context->getPsrQueue());
+            ->method('onStart')
+            ->willReturnCallback(function (Start $context) {
+                $context->interruptExecution();
             })
         ;
+        $extension
+            ->expects($this->once())
+            ->method('onEnd')
+        ;
+        $extension
+            ->expects($this->never())
+            ->method('onPreConsume')
+        ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind($queue, $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, [], $expectedLogger);
+        $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer());
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
 
-    public function testShouldCallOnPreReceivedExtensionMethodWithExpectedContext()
+    public function testShouldCallPreSubscribeExtensionMethod()
     {
-        $expectedMessage = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage);
+        $consumerStub = $this->createConsumerStub('foo_queue');
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $contextStub = $this->createContextStub($consumerStub);
 
-        $processorMock = $this->createProcessorStub();
+        $processorMock = $this->createProcessorMock();
 
         $extension = $this->createExtension();
         $extension
             ->expects($this->once())
-            ->method('onPreReceived')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use (
-                $contextStub,
-                $messageConsumerStub,
-                $processorMock,
-                $expectedMessage
-            ) {
-                $this->assertSame($contextStub, $context->getPsrContext());
-                $this->assertSame($messageConsumerStub, $context->getPsrConsumer());
-                $this->assertSame($processorMock, $context->getPsrProcessor());
-                $this->assertSame($expectedMessage, $context->getPsrMessage());
+            ->method('onPreSubscribe')
+            ->with($this->isInstanceOf(PreSubscribe::class))
+            ->willReturnCallback(function (PreSubscribe $context) use ($contextStub, $consumerStub, $processorMock) {
+                $this->assertSame($contextStub, $context->getContext());
+                $this->assertSame($consumerStub, $context->getConsumer());
+                $this->assertSame($processorMock, $context->getProcessor());
                 $this->assertInstanceOf(NullLogger::class, $context->getLogger());
-                $this->assertNull($context->getException());
-                $this->assertNull($context->getResult());
-                $this->assertFalse($context->isExecutionInterrupted());
             })
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer());
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
 
-    public function testShouldCallOnResultExtensionMethodWithExpectedContext()
+    public function testShouldCallPreSubscribeForEachBoundProcessor()
     {
-        $expectedMessage = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage);
+        $consumerStub = $this->createConsumerStub('foo_queue');
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $contextStub = $this->createContextStub($consumerStub);
 
-        $processorMock = $this->createProcessorStub();
+        $processorMock = $this->createProcessorMock();
+
+        $extension = $this->createExtension();
+        $extension
+            ->expects($this->exactly(3))
+            ->method('onPreSubscribe')
+            ->with($this->isInstanceOf(PreSubscribe::class))
+        ;
+
+        $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer());
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
+        $queueConsumer->bind(new NullQueue('bar_queue'), $processorMock);
+        $queueConsumer->bind(new NullQueue('baz_queue'), $processorMock);
+
+        $queueConsumer->consume();
+    }
+
+    public function testShouldCallOnPostConsumeExtensionMethod()
+    {
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
+
+        $subscriptionConsumer = new DummySubscriptionConsumer();
+
+        $processorMock = $this->createProcessorMock();
 
         $extension = $this->createExtension();
         $extension
             ->expects($this->once())
-            ->method('onResult')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use (
-                $contextStub,
-                $messageConsumerStub,
-                $processorMock,
-                $expectedMessage
-            ) {
-                $this->assertSame($contextStub, $context->getPsrContext());
-                $this->assertSame($messageConsumerStub, $context->getPsrConsumer());
-                $this->assertSame($processorMock, $context->getPsrProcessor());
-                $this->assertSame($expectedMessage, $context->getPsrMessage());
+            ->method('onPostConsume')
+            ->with($this->isInstanceOf(PostConsume::class))
+            ->willReturnCallback(function (PostConsume $context) use ($contextStub, $subscriptionConsumer) {
+                $this->assertSame($contextStub, $context->getContext());
+                $this->assertSame($subscriptionConsumer, $context->getSubscriptionConsumer());
+                $this->assertSame(1, $context->getCycle());
+                $this->assertSame(0, $context->getReceivedMessagesCount());
+                $this->assertGreaterThan(1, $context->getStartTime());
                 $this->assertInstanceOf(NullLogger::class, $context->getLogger());
-                $this->assertNull($context->getException());
-                $this->assertSame(Result::ACK, $context->getResult());
                 $this->assertFalse($context->isExecutionInterrupted());
             })
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumer);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
 
-    public function testShouldCallOnPostReceivedExtensionMethodWithExpectedContext()
+    public function testShouldCallOnPreConsumeExtensionMethod()
     {
-        $expectedMessage = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage);
+        $consumerStub = $this->createConsumerStub('foo_queue');
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorStub();
 
+        $queue = new NullQueue('foo_queue');
+
         $extension = $this->createExtension();
         $extension
             ->expects($this->once())
-            ->method('onPostReceived')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use (
-                $contextStub,
-                $messageConsumerStub,
-                $processorMock,
-                $expectedMessage
-            ) {
-                $this->assertSame($contextStub, $context->getPsrContext());
-                $this->assertSame($messageConsumerStub, $context->getPsrConsumer());
-                $this->assertSame($processorMock, $context->getPsrProcessor());
-                $this->assertSame($expectedMessage, $context->getPsrMessage());
+            ->method('onPreConsume')
+            ->with($this->isInstanceOf(PreConsume::class))
+            ->willReturnCallback(function (PreConsume $context) use ($contextStub) {
+                $this->assertSame($contextStub, $context->getContext());
                 $this->assertInstanceOf(NullLogger::class, $context->getLogger());
-                $this->assertNull($context->getException());
-                $this->assertSame(Result::ACK, $context->getResult());
+                $this->assertInstanceOf(SubscriptionConsumer::class, $context->getSubscriptionConsumer());
+                $this->assertSame(10000, $context->getReceiveTimeout());
+                $this->assertSame(1, $context->getCycle());
+                $this->assertGreaterThan(0, $context->getStartTime());
                 $this->assertFalse($context->isExecutionInterrupted());
             })
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer());
+        $queueConsumer->bind($queue, $processorMock);
 
         $queueConsumer->consume();
     }
 
-    public function testShouldAllowInterruptConsumingOnIdle()
+    public function testShouldCallOnPreConsumeExpectedAmountOfTimes()
     {
-        $messageConsumerStub = $this->createMessageConsumerStub($message = null);
+        $consumerStub = $this->createConsumerStub('foo_queue');
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $contextStub = $this->createContextStub($consumerStub);
 
-        $processorMock = $this->createProcessorMock();
+        $processorMock = $this->createProcessorStub();
+
+        $queue = new NullQueue('foo_queue');
 
         $extension = $this->createExtension();
         $extension
-            ->expects($this->once())
-            ->method('onIdle')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) {
-                $context->setExecutionInterrupted(true);
-            })
+            ->expects($this->exactly(3))
+            ->method('onPreConsume')
         ;
+
+        $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(3)]);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer());
+        $queueConsumer->bind($queue, $processorMock);
+
+        $queueConsumer->consume();
+    }
+
+    public function testShouldCallOnPreReceivedExtensionMethodWithExpectedContext()
+    {
+        $expectedMessage = $this->createMessageMock();
+
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
+
+        $processorMock = $this->createProcessorStub();
+
+        $extension = $this->createExtension();
         $extension
             ->expects($this->once())
-            ->method('onInterrupted')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use (
+            ->method('onMessageReceived')
+            ->with($this->isInstanceOf(MessageReceived::class))
+            ->willReturnCallback(function (MessageReceived $context) use (
                 $contextStub,
-                $messageConsumerStub,
-                $processorMock
+                $consumerStub,
+                $processorMock,
+                $expectedMessage
             ) {
-                $this->assertSame($contextStub, $context->getPsrContext());
-                $this->assertSame($messageConsumerStub, $context->getPsrConsumer());
-                $this->assertSame($processorMock, $context->getPsrProcessor());
+                $this->assertSame($contextStub, $context->getContext());
+                $this->assertSame($consumerStub, $context->getConsumer());
+                $this->assertSame($processorMock, $context->getProcessor());
+                $this->assertSame($expectedMessage, $context->getMessage());
                 $this->assertInstanceOf(NullLogger::class, $context->getLogger());
-                $this->assertNull($context->getPsrMessage());
-                $this->assertNull($context->getException());
                 $this->assertNull($context->getResult());
-                $this->assertTrue($context->isExecutionInterrupted());
             })
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
 
-    public function testShouldNotCloseContextWhenConsumptionInterrupted()
+    public function testShouldCallOnResultExtensionMethodWithExpectedContext()
     {
-        $messageConsumerStub = $this->createMessageConsumerStub($message = null);
+        $expectedMessage = $this->createMessageMock();
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
-        $contextStub
-            ->expects($this->never())
-            ->method('close')
-        ;
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue');
 
-        $processorMock = $this->createProcessorMock();
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
+
+        $processorMock = $this->createProcessorStub();
 
         $extension = $this->createExtension();
         $extension
             ->expects($this->once())
-            ->method('onIdle')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) {
-                $context->setExecutionInterrupted(true);
+            ->method('onResult')
+            ->with($this->isInstanceOf(MessageResult::class))
+            ->willReturnCallback(function (MessageResult $context) use ($contextStub, $expectedMessage) {
+                $this->assertSame($contextStub, $context->getContext());
+                $this->assertSame($expectedMessage, $context->getMessage());
+                $this->assertInstanceOf(NullLogger::class, $context->getLogger());
+                $this->assertSame(Result::ACK, $context->getResult());
             })
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
 
-    public function testShouldNotCloseContextWhenConsumptionInterruptedByException()
+    public function testShouldCallOnProcessorExceptionExtensionMethodWithExpectedContext()
     {
-        $expectedException = new \Exception();
+        $exception = new \LogicException('Exception exception');
 
-        $messageConsumerStub = $this->createMessageConsumerStub($message = $this->createMessageMock());
+        $expectedMessage = $this->createMessageMock();
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
-        $contextStub
-            ->expects($this->never())
-            ->method('close')
-        ;
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue');
 
-        $processorMock = $this->createProcessorMock();
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
+
+        $processorMock = $this->createProcessorStub();
         $processorMock
             ->expects($this->once())
             ->method('process')
-            ->willThrowException($expectedException)
+            ->willThrowException($exception)
         ;
 
-        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
-
-        try {
-            $queueConsumer->consume();
-        } catch (\Exception $e) {
-            $this->assertSame($expectedException, $e);
-            $this->assertNull($e->getPrevious());
+        $extension = $this->createExtension();
+        $extension
+            ->expects($this->never())
+            ->method('onResult')
+        ;
+        $extension
+            ->expects($this->once())
+            ->method('onProcessorException')
+            ->with($this->isInstanceOf(ProcessorException::class))
+            ->willReturnCallback(function (ProcessorException $context) use ($contextStub, $expectedMessage, $exception) {
+                $this->assertSame($contextStub, $context->getContext());
+                $this->assertSame($expectedMessage, $context->getMessage());
+                $this->assertSame($exception, $context->getException());
+                $this->assertGreaterThan(1, $context->getReceivedAt());
+                $this->assertInstanceOf(NullLogger::class, $context->getLogger());
+                $this->assertNull($context->getResult());
+            })
+        ;
 
-            return;
-        }
+        $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
-        $this->fail('Exception throw is expected.');
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Exception exception');
+        $queueConsumer->consume();
     }
 
-    public function testShouldSetMainExceptionAsPreviousToExceptionThrownOnInterrupt()
+    public function testShouldContinueConsumptionIfResultSetOnProcessorExceptionExtension()
     {
-        $mainException = new \Exception();
-        $expectedException = new \Exception();
+        $result = Result::ack();
 
-        $messageConsumerStub = $this->createMessageConsumerStub($message = $this->createMessageMock());
+        $expectedMessage = $this->createMessageMock();
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue');
 
-        $processorMock = $this->createProcessorMock();
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
+
+        $processorMock = $this->createProcessorStub();
         $processorMock
             ->expects($this->once())
             ->method('process')
-            ->willThrowException($mainException)
+            ->willThrowException(new \LogicException())
         ;
 
         $extension = $this->createExtension();
         $extension
-            ->expects($this->atLeastOnce())
-            ->method('onInterrupted')
-            ->willThrowException($expectedException)
+            ->expects($this->once())
+            ->method('onProcessorException')
+            ->willReturnCallback(function (ProcessorException $context) use ($result) {
+                $context->setResult($result);
+            })
+        ;
+        $extension
+            ->expects($this->once())
+            ->method('onResult')
+            ->willReturnCallback(function (MessageResult $context) use ($result) {
+                $this->assertSame($result, $context->getResult());
+            })
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
-        try {
-            $queueConsumer->consume();
-        } catch (\Exception $e) {
-            $this->assertSame($expectedException, $e);
-            $this->assertSame($mainException, $e->getPrevious());
-
-            return;
-        }
-
-        $this->fail('Exception throw is expected.');
+        $queueConsumer->consume();
     }
 
-    public function testShouldAllowInterruptConsumingOnPreReceiveButProcessCurrentMessage()
+    public function testShouldCallOnPostMessageReceivedExtensionMethodWithExpectedContext()
     {
         $expectedMessage = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage);
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue');
 
-        $processorMock = $this->createProcessorMock();
-        $processorMock
-            ->expects($this->once())
-            ->method('process')
-            ->willReturn(Result::ACK)
-        ;
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
+
+        $processorMock = $this->createProcessorStub();
 
         $extension = $this->createExtension();
         $extension
             ->expects($this->once())
-            ->method('onPreReceived')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) {
-                $context->setExecutionInterrupted(true);
-            })
-        ;
-        $extension
-            ->expects($this->atLeastOnce())
-            ->method('onInterrupted')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use (
+            ->method('onPostMessageReceived')
+            ->with($this->isInstanceOf(PostMessageReceived::class))
+            ->willReturnCallback(function (PostMessageReceived $context) use (
                 $contextStub,
-                $messageConsumerStub,
-                $processorMock,
                 $expectedMessage
             ) {
-                $this->assertSame($contextStub, $context->getPsrContext());
-                $this->assertSame($messageConsumerStub, $context->getPsrConsumer());
-                $this->assertSame($processorMock, $context->getPsrProcessor());
-                $this->assertSame($expectedMessage, $context->getPsrMessage());
+                $this->assertSame($contextStub, $context->getContext());
+                $this->assertSame($expectedMessage, $context->getMessage());
                 $this->assertInstanceOf(NullLogger::class, $context->getLogger());
-                $this->assertNull($context->getException());
                 $this->assertSame(Result::ACK, $context->getResult());
-                $this->assertTrue($context->isExecutionInterrupted());
+                $this->assertFalse($context->isExecutionInterrupted());
             })
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
 
-    public function testShouldAllowInterruptConsumingOnResult()
+    public function testShouldAllowInterruptConsumingOnPostConsume()
     {
-        $expectedMessage = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage);
+        $consumerStub = $this->createConsumerStub('foo_queue');
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
-        $processorMock
-            ->expects($this->once())
-            ->method('process')
-            ->willReturn(Result::ACK)
-        ;
 
         $extension = $this->createExtension();
         $extension
             ->expects($this->once())
-            ->method('onResult')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) {
-                $context->setExecutionInterrupted(true);
+            ->method('onPostConsume')
+            ->with($this->isInstanceOf(PostConsume::class))
+            ->willReturnCallback(function (PostConsume $context) {
+                $context->interruptExecution();
             })
         ;
         $extension
-            ->expects($this->atLeastOnce())
-            ->method('onInterrupted')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use (
-                $contextStub,
-                $messageConsumerStub,
-                $processorMock,
-                $expectedMessage
-            ) {
-                $this->assertSame($contextStub, $context->getPsrContext());
-                $this->assertSame($messageConsumerStub, $context->getPsrConsumer());
-                $this->assertSame($processorMock, $context->getPsrProcessor());
-                $this->assertSame($expectedMessage, $context->getPsrMessage());
+            ->expects($this->once())
+            ->method('onEnd')
+            ->with($this->isInstanceOf(End::class))
+            ->willReturnCallback(function (End $context) use ($contextStub) {
+                $this->assertSame($contextStub, $context->getContext());
                 $this->assertInstanceOf(NullLogger::class, $context->getLogger());
-                $this->assertNull($context->getException());
-                $this->assertSame(Result::ACK, $context->getResult());
-                $this->assertTrue($context->isExecutionInterrupted());
+                $this->assertGreaterThan(1, $context->getStartTime());
+                $this->assertGreaterThan(1, $context->getEndTime());
             })
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer());
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
 
-    public function testShouldAllowInterruptConsumingOnPostReceive()
+    public function testShouldSetMainExceptionAsPreviousToExceptionThrownOnProcessorException()
+    {
+        $mainException = new \Exception();
+        $expectedException = new \Exception();
+
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($this->createMessageMock(), 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
+
+        $processorMock = $this->createProcessorMock();
+        $processorMock
+            ->expects($this->once())
+            ->method('process')
+            ->willThrowException($mainException)
+        ;
+
+        $extension = $this->createExtension();
+        $extension
+            ->expects($this->atLeastOnce())
+            ->method('onProcessorException')
+            ->willThrowException($expectedException)
+        ;
+
+        $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
+
+        try {
+            $queueConsumer->consume();
+        } catch (\Exception $e) {
+            $this->assertSame($expectedException, $e);
+            $this->assertSame($mainException, $e->getPrevious());
+
+            return;
+        }
+
+        $this->fail('Exception throw is expected.');
+    }
+
+    public function testShouldAllowInterruptConsumingOnPostMessageReceived()
     {
         $expectedMessage = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage);
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
         $processorMock
@@ -891,47 +1152,37 @@ public function testShouldAllowInterruptConsumingOnPostReceive()
         $extension = $this->createExtension();
         $extension
             ->expects($this->once())
-            ->method('onPostReceived')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) {
-                $context->setExecutionInterrupted(true);
+            ->method('onPostMessageReceived')
+            ->with($this->isInstanceOf(PostMessageReceived::class))
+            ->willReturnCallback(function (PostMessageReceived $context) {
+                $context->interruptExecution();
             })
         ;
         $extension
             ->expects($this->atLeastOnce())
-            ->method('onInterrupted')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use (
-                $contextStub,
-                $messageConsumerStub,
-                $processorMock,
-                $expectedMessage
-            ) {
-                $this->assertSame($contextStub, $context->getPsrContext());
-                $this->assertSame($messageConsumerStub, $context->getPsrConsumer());
-                $this->assertSame($processorMock, $context->getPsrProcessor());
-                $this->assertSame($expectedMessage, $context->getPsrMessage());
-                $this->assertInstanceOf(NullLogger::class, $context->getLogger());
-                $this->assertNull($context->getException());
-                $this->assertSame(Result::ACK, $context->getResult());
-                $this->assertTrue($context->isExecutionInterrupted());
-            })
+            ->method('onEnd')
+            ->with($this->isInstanceOf(End::class))
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
 
-    public function testShouldCallOnInterruptedIfExceptionThrow()
+    public function testShouldNotCallOnEndIfExceptionThrow()
     {
         $expectedException = new \Exception('Process failed');
         $expectedMessage = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage);
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
         $processorMock
@@ -942,30 +1193,14 @@ public function testShouldCallOnInterruptedIfExceptionThrow()
 
         $extension = $this->createExtension();
         $extension
-            ->expects($this->atLeastOnce())
-            ->method('onInterrupted')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use (
-                $contextStub,
-                $messageConsumerStub,
-                $processorMock,
-                $expectedMessage,
-                $expectedException
-            ) {
-                $this->assertSame($contextStub, $context->getPsrContext());
-                $this->assertSame($messageConsumerStub, $context->getPsrConsumer());
-                $this->assertSame($processorMock, $context->getPsrProcessor());
-                $this->assertSame($expectedMessage, $context->getPsrMessage());
-                $this->assertSame($expectedException, $context->getException());
-                $this->assertInstanceOf(NullLogger::class, $context->getLogger());
-                $this->assertNull($context->getResult());
-                $this->assertTrue($context->isExecutionInterrupted());
-            })
+            ->expects($this->never())
+            ->method('onEnd')
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $this->expectException(\Exception::class);
         $this->expectExceptionMessage('Process failed');
@@ -975,9 +1210,13 @@ public function testShouldCallOnInterruptedIfExceptionThrow()
     public function testShouldCallExtensionPassedOnRuntime()
     {
         $expectedMessage = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage);
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
         $processorMock
@@ -987,44 +1226,59 @@ public function testShouldCallExtensionPassedOnRuntime()
         ;
 
         $runtimeExtension = $this->createExtension();
+        $runtimeExtension
+            ->expects($this->once())
+            ->method('onInitLogger')
+            ->with($this->isInstanceOf(InitLogger::class))
+        ;
         $runtimeExtension
             ->expects($this->once())
             ->method('onStart')
-            ->with($this->isInstanceOf(Context::class))
+            ->with($this->isInstanceOf(Start::class))
         ;
         $runtimeExtension
             ->expects($this->once())
-            ->method('onBeforeReceive')
-            ->with($this->isInstanceOf(Context::class))
+            ->method('onPreSubscribe')
+            ->with($this->isInstanceOf(PreSubscribe::class))
         ;
         $runtimeExtension
             ->expects($this->once())
-            ->method('onPreReceived')
-            ->with($this->isInstanceOf(Context::class))
+            ->method('onPreConsume')
+            ->with($this->isInstanceOf(PreConsume::class))
+        ;
+        $runtimeExtension
+            ->expects($this->once())
+            ->method('onMessageReceived')
+            ->with($this->isInstanceOf(MessageReceived::class))
         ;
         $runtimeExtension
             ->expects($this->once())
             ->method('onResult')
-            ->with($this->isInstanceOf(Context::class))
+            ->with($this->isInstanceOf(MessageResult::class))
         ;
         $runtimeExtension
             ->expects($this->once())
-            ->method('onPostReceived')
-            ->with($this->isInstanceOf(Context::class))
+            ->method('onPostMessageReceived')
+            ->with($this->isInstanceOf(PostMessageReceived::class))
         ;
 
-        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1));
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume(new ChainExtension([$runtimeExtension]));
     }
 
-    public function testShouldChangeLoggerOnStart()
+    public function testShouldChangeLoggerOnInitLogger()
     {
         $expectedMessage = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage);
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue');
+
+        $consumerStub = $this->createConsumerStub('foo_queue');
+
+        $contextStub = $this->createContextStub($consumerStub);
 
         $processorMock = $this->createProcessorMock();
         $processorMock
@@ -1036,136 +1290,204 @@ public function testShouldChangeLoggerOnStart()
         $expectedLogger = new NullLogger();
 
         $extension = $this->createExtension();
+        $extension
+            ->expects($this->atLeastOnce())
+            ->method('onInitLogger')
+            ->with($this->isInstanceOf(InitLogger::class))
+            ->willReturnCallback(function (InitLogger $context) use ($expectedLogger) {
+                $context->changeLogger($expectedLogger);
+            })
+        ;
         $extension
             ->expects($this->atLeastOnce())
             ->method('onStart')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use ($expectedLogger) {
-                $context->setLogger($expectedLogger);
+            ->with($this->isInstanceOf(Start::class))
+            ->willReturnCallback(function (Start $context) use ($expectedLogger) {
+                $this->assertSame($expectedLogger, $context->getLogger());
+            })
+        ;
+        $extension
+            ->expects($this->atLeastOnce())
+            ->method('onPreSubscribe')
+            ->with($this->isInstanceOf(PreSubscribe::class))
+            ->willReturnCallback(function (PreSubscribe $context) use ($expectedLogger) {
+                $this->assertSame($expectedLogger, $context->getLogger());
             })
         ;
         $extension
             ->expects($this->atLeastOnce())
-            ->method('onBeforeReceive')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use ($expectedLogger) {
+            ->method('onPreConsume')
+            ->with($this->isInstanceOf(PreConsume::class))
+            ->willReturnCallback(function (PreConsume $context) use ($expectedLogger) {
                 $this->assertSame($expectedLogger, $context->getLogger());
             })
         ;
         $extension
             ->expects($this->atLeastOnce())
-            ->method('onPreReceived')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use ($expectedLogger) {
+            ->method('onMessageReceived')
+            ->with($this->isInstanceOf(MessageReceived::class))
+            ->willReturnCallback(function (MessageReceived $context) use ($expectedLogger) {
                 $this->assertSame($expectedLogger, $context->getLogger());
             })
         ;
 
         $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]);
 
-        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0);
-        $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock);
+        $queueConsumer = new QueueConsumer($contextStub, $chainExtensions);
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
+        $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock);
 
         $queueConsumer->consume();
     }
 
-    public function testShouldCallEachQueueOneByOne()
+    public function testShouldCallProcessorAsMessageComeAlong()
     {
-        $expectedMessage = $this->createMessageMock();
-        $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage);
+        $queue1 = new NullQueue('foo_queue');
+        $queue2 = new NullQueue('bar_queue');
+
+        $firstMessage = $this->createMessageMock();
+        $secondMessage = $this->createMessageMock();
+        $thirdMessage = $this->createMessageMock();
+
+        $subscriptionConsumerMock = new DummySubscriptionConsumer();
+        $subscriptionConsumerMock->addMessage($firstMessage, 'foo_queue');
+        $subscriptionConsumerMock->addMessage($secondMessage, 'bar_queue');
+        $subscriptionConsumerMock->addMessage($thirdMessage, 'foo_queue');
+
+        $fooConsumerStub = $this->createConsumerStub($queue1);
+        $barConsumerStub = $this->createConsumerStub($queue2);
 
-        $contextStub = $this->createPsrContextStub($messageConsumerStub);
+        $consumers = [
+            'foo_queue' => $fooConsumerStub,
+            'bar_queue' => $barConsumerStub,
+        ];
+
+        $contextStub = $this->createContextWithoutSubscriptionConsumerMock();
+        $contextStub
+            ->expects($this->any())
+            ->method('createQueue')
+            ->willReturnCallback(function (string $queueName) {
+                return new NullQueue($queueName);
+            })
+        ;
+        $contextStub
+            ->expects($this->any())
+            ->method('createConsumer')
+            ->willReturnCallback(function (Queue $queue) use ($consumers) {
+                return $consumers[$queue->getQueueName()];
+            })
+        ;
 
         $processorMock = $this->createProcessorStub();
         $anotherProcessorMock = $this->createProcessorStub();
 
-        $queue1 = new NullQueue('aQueueName');
-        $queue2 = new NullQueue('aAnotherQueueName');
+        /** @var MessageReceived[] $actualContexts */
+        $actualContexts = [];
 
         $extension = $this->createExtension();
         $extension
-            ->expects($this->at(1))
-            ->method('onBeforeReceive')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use ($processorMock, $queue1) {
-                $this->assertSame($processorMock, $context->getPsrProcessor());
-                $this->assertSame($queue1, $context->getPsrQueue());
-            })
-        ;
-        $extension
-            ->expects($this->at(5))
-            ->method('onBeforeReceive')
-            ->with($this->isInstanceOf(Context::class))
-            ->willReturnCallback(function (Context $context) use ($anotherProcessorMock, $queue2) {
-                $this->assertSame($anotherProcessorMock, $context->getPsrProcessor());
-                $this->assertSame($queue2, $context->getPsrQueue());
+            ->expects($this->any())
+            ->method('onMessageReceived')
+            ->with($this->isInstanceOf(MessageReceived::class))
+            ->willReturnCallback(function (MessageReceived $context) use (&$actualContexts) {
+                $actualContexts[] = clone $context;
             })
         ;
 
-        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(2), 0);
+        $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(3));
+        $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock);
         $queueConsumer
             ->bind($queue1, $processorMock)
             ->bind($queue2, $anotherProcessorMock)
         ;
 
         $queueConsumer->consume(new ChainExtension([$extension]));
+
+        $this->assertCount(3, $actualContexts);
+
+        $this->assertSame($firstMessage, $actualContexts[0]->getMessage());
+        $this->assertSame($secondMessage, $actualContexts[1]->getMessage());
+        $this->assertSame($thirdMessage, $actualContexts[2]->getMessage());
+
+        $this->assertSame($fooConsumerStub, $actualContexts[0]->getConsumer());
+        $this->assertSame($barConsumerStub, $actualContexts[1]->getConsumer());
+        $this->assertSame($fooConsumerStub, $actualContexts[2]->getConsumer());
+    }
+
+    public function testCaptureExitStatus()
+    {
+        $testExitCode = 5;
+
+        $stubExtension = $this->createExtension();
+
+        $stubExtension
+            ->expects($this->once())
+            ->method('onStart')
+            ->with($this->isInstanceOf(Start::class))
+            ->willReturnCallback(function (Start $context) use ($testExitCode) {
+                $context->interruptExecution($testExitCode);
+            })
+        ;
+
+        $exitExtension = new ExitStatusExtension();
+
+        $consumer = new QueueConsumer($this->createContextStub(), $stubExtension);
+        $consumer->consume(new ChainExtension([$exitExtension]));
+
+        $this->assertEquals($testExitCode, $exitExtension->getExitStatus());
     }
 
     /**
-     * @param null|mixed $message
-     *
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrConsumer
+     * @return \PHPUnit\Framework\MockObject\MockObject
      */
-    protected function createMessageConsumerStub($message = null)
+    private function createContextWithoutSubscriptionConsumerMock(): InteropContext
     {
-        $messageConsumerMock = $this->createMock(PsrConsumer::class);
-        $messageConsumerMock
+        $contextMock = $this->createMock(InteropContext::class);
+        $contextMock
             ->expects($this->any())
-            ->method('receive')
-            ->willReturn($message)
+            ->method('createSubscriptionConsumer')
+            ->willThrowException(SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt())
         ;
 
-        return $messageConsumerMock;
+        return $contextMock;
     }
 
     /**
-     * @param null|mixed $messageConsumer
-     *
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext
+     * @return \PHPUnit\Framework\MockObject\MockObject|InteropContext
      */
-    protected function createPsrContextStub($messageConsumer = null)
+    private function createContextStub(?Consumer $consumer = null): InteropContext
     {
-        $context = $this->createMock(PsrContext::class);
-        $context
-            ->expects($this->any())
-            ->method('createConsumer')
-            ->willReturn($messageConsumer)
-        ;
+        $context = $this->createContextWithoutSubscriptionConsumerMock();
         $context
             ->expects($this->any())
             ->method('createQueue')
-            ->willReturn($this->createMock(PsrQueue::class))
+            ->willReturnCallback(function (string $queueName) {
+                return new NullQueue($queueName);
+            })
         ;
         $context
             ->expects($this->any())
-            ->method('close')
+            ->method('createConsumer')
+            ->willReturnCallback(function (Queue $queue) use ($consumer) {
+                return $consumer ?: $this->createConsumerStub($queue);
+            })
         ;
 
         return $context;
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor
+     * @return \PHPUnit\Framework\MockObject\MockObject|Processor
      */
-    protected function createProcessorMock()
+    private function createProcessorMock()
     {
-        return $this->createMock(PsrProcessor::class);
+        return $this->createMock(Processor::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor
+     * @return \PHPUnit\Framework\MockObject\MockObject|Processor
      */
-    protected function createProcessorStub()
+    private function createProcessorStub()
     {
         $processorMock = $this->createProcessorMock();
         $processorMock
@@ -1178,18 +1500,50 @@ protected function createProcessorStub()
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrMessage
+     * @return \PHPUnit\Framework\MockObject\MockObject|Message
      */
-    protected function createMessageMock()
+    private function createMessageMock(): Message
     {
-        return $this->createMock(PsrMessage::class);
+        return $this->createMock(Message::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|ExtensionInterface
+     * @return \PHPUnit\Framework\MockObject\MockObject|ExtensionInterface
      */
-    protected function createExtension()
+    private function createExtension()
     {
         return $this->createMock(ExtensionInterface::class);
     }
+
+    /**
+     * @param mixed|null $queue
+     *
+     * @return \PHPUnit\Framework\MockObject\MockObject|Consumer
+     */
+    private function createConsumerStub($queue = null): Consumer
+    {
+        if (null === $queue) {
+            $queue = 'queue';
+        }
+        if (is_string($queue)) {
+            $queue = new NullQueue($queue);
+        }
+
+        $consumerMock = $this->createMock(Consumer::class);
+        $consumerMock
+            ->expects($this->any())
+            ->method('getQueue')
+            ->willReturn($queue)
+        ;
+
+        return $consumerMock;
+    }
+
+    /**
+     * @return SubscriptionConsumer|\PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createSubscriptionConsumerMock(): SubscriptionConsumer
+    {
+        return $this->createMock(SubscriptionConsumer::class);
+    }
 }
diff --git a/Tests/Consumption/ResultTest.php b/Tests/Consumption/ResultTest.php
index 94abb9e..26b8c78 100644
--- a/Tests/Consumption/ResultTest.php
+++ b/Tests/Consumption/ResultTest.php
@@ -14,13 +14,13 @@ public function testCouldBeConstructedWithExpectedArguments()
 
         $this->assertSame('theStatus', $result->getStatus());
         $this->assertSame('', $result->getReason());
-        $this->assertSame(null, $result->getReply());
+        $this->assertNull($result->getReply());
 
         $result = new Result('theStatus', 'theReason');
 
         $this->assertSame('theStatus', $result->getStatus());
         $this->assertSame('theReason', $result->getReason());
-        $this->assertSame(null, $result->getReply());
+        $this->assertNull($result->getReply());
     }
 
     public function testCouldConstructedWithAckFactoryMethod()
@@ -30,7 +30,7 @@ public function testCouldConstructedWithAckFactoryMethod()
         $this->assertInstanceOf(Result::class, $result);
         $this->assertSame(Result::ACK, $result->getStatus());
         $this->assertSame('theReason', $result->getReason());
-        $this->assertSame(null, $result->getReply());
+        $this->assertNull($result->getReply());
     }
 
     public function testCouldConstructedWithRejectFactoryMethod()
@@ -40,7 +40,7 @@ public function testCouldConstructedWithRejectFactoryMethod()
         $this->assertInstanceOf(Result::class, $result);
         $this->assertSame(Result::REJECT, $result->getStatus());
         $this->assertSame('theReason', $result->getReason());
-        $this->assertSame(null, $result->getReply());
+        $this->assertNull($result->getReply());
     }
 
     public function testCouldConstructedWithRequeueFactoryMethod()
@@ -50,7 +50,7 @@ public function testCouldConstructedWithRequeueFactoryMethod()
         $this->assertInstanceOf(Result::class, $result);
         $this->assertSame(Result::REQUEUE, $result->getStatus());
         $this->assertSame('theReason', $result->getReason());
-        $this->assertSame(null, $result->getReply());
+        $this->assertNull($result->getReply());
     }
 
     public function testCouldConstructedWithReplyFactoryMethodAndAckStatusByDefault()
diff --git a/Tests/DoctrineConnectionFactoryFactoryTest.php b/Tests/DoctrineConnectionFactoryFactoryTest.php
new file mode 100644
index 0000000..14f7b10
--- /dev/null
+++ b/Tests/DoctrineConnectionFactoryFactoryTest.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Enqueue\Tests;
+
+use Doctrine\Persistence\ManagerRegistry;
+use Enqueue\ConnectionFactoryFactoryInterface;
+use Enqueue\Dbal\ManagerRegistryConnectionFactory;
+use Enqueue\Doctrine\DoctrineConnectionFactoryFactory;
+use PHPUnit\Framework\TestCase;
+use Prophecy\PhpUnit\ProphecyTrait;
+
+class DoctrineConnectionFactoryFactoryTest extends TestCase
+{
+    use ProphecyTrait;
+
+    /**
+     * @var ManagerRegistry|\Prophecy\Prophecy\ObjectProphecy
+     */
+    private $registry;
+    /**
+     * @var ConnectionFactoryFactoryInterface|\Prophecy\Prophecy\ObjectProphecy
+     */
+    private $fallbackFactory;
+    /**
+     * @var DoctrineConnectionFactoryFactory
+     */
+    private $factory;
+
+    protected function setUp(): void
+    {
+        $this->registry = $this->prophesize(ManagerRegistry::class);
+        $this->fallbackFactory = $this->prophesize(ConnectionFactoryFactoryInterface::class);
+
+        $this->factory = new DoctrineConnectionFactoryFactory($this->registry->reveal(), $this->fallbackFactory->reveal());
+    }
+
+    public function testCreateWithoutArray()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The config must be either array or DSN string.');
+
+        $this->factory->create(true);
+    }
+
+    public function testCreateWithoutDsn()
+    {
+        $this->expectExceptionMessage(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The config must have dsn key set.');
+
+        $this->factory->create(['foo' => 'bar']);
+    }
+
+    public function testCreateWithDoctrineSchema()
+    {
+        $this->assertInstanceOf(
+            ManagerRegistryConnectionFactory::class,
+            $this->factory->create('doctrine://localhost:3306')
+        );
+    }
+
+    public function testCreateFallback()
+    {
+        $this->fallbackFactory
+            ->create(['dsn' => 'fallback://'])
+            ->shouldBeCalled();
+
+        $this->factory->create(['dsn' => 'fallback://']);
+    }
+}
diff --git a/Tests/Functions/DsnToConnectionFactoryFunctionTest.php b/Tests/Functions/DsnToConnectionFactoryFunctionTest.php
deleted file mode 100644
index 59c5333..0000000
--- a/Tests/Functions/DsnToConnectionFactoryFunctionTest.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Functions;
-
-use Enqueue\AmqpExt\AmqpConnectionFactory;
-use Enqueue\Dbal\DbalConnectionFactory;
-use Enqueue\Fs\FsConnectionFactory;
-use Enqueue\Gearman\GearmanConnectionFactory;
-use Enqueue\Gps\GpsConnectionFactory;
-use Enqueue\Null\NullConnectionFactory;
-use Enqueue\Pheanstalk\PheanstalkConnectionFactory;
-use Enqueue\RdKafka\RdKafkaConnectionFactory;
-use Enqueue\Redis\RedisConnectionFactory;
-use Enqueue\Sqs\SqsConnectionFactory;
-use Enqueue\Stomp\StompConnectionFactory;
-use PHPUnit\Framework\TestCase;
-
-class DsnToConnectionFactoryFunctionTest extends TestCase
-{
-    public function testThrowIfDsnEmpty()
-    {
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The scheme could not be parsed from DSN ""');
-
-        \Enqueue\dsn_to_connection_factory('');
-    }
-
-    public function testThrowIfDsnMissingScheme()
-    {
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The scheme could not be parsed from DSN "dsnMissingScheme"');
-
-        \Enqueue\dsn_to_connection_factory('dsnMissingScheme');
-    }
-
-    public function testThrowIfDsnNotSupported()
-    {
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The scheme "http" is not supported. Supported "file", "amqp+ext"');
-
-        \Enqueue\dsn_to_connection_factory('http://schemeNotSupported');
-    }
-
-    /**
-     * @dataProvider provideDSNs
-     *
-     * @param mixed $dsn
-     * @param mixed $expectedFactoryClass
-     */
-    public function testReturnsExpectedFactoryInstance($dsn, $expectedFactoryClass)
-    {
-        $factory = \Enqueue\dsn_to_connection_factory($dsn);
-
-        $this->assertInstanceOf($expectedFactoryClass, $factory);
-    }
-
-    public static function provideDSNs()
-    {
-        yield ['amqp:', AmqpConnectionFactory::class];
-
-        yield ['amqp://user:pass@foo/vhost', AmqpConnectionFactory::class];
-
-        yield ['file:', FsConnectionFactory::class];
-
-        yield ['file:///foo/bar/baz', FsConnectionFactory::class];
-
-        yield ['null:', NullConnectionFactory::class];
-
-        yield ['mysql:', DbalConnectionFactory::class];
-
-        yield ['pgsql:', DbalConnectionFactory::class];
-
-        yield ['beanstalk:', PheanstalkConnectionFactory::class];
-
-        //        yield ['gearman:', GearmanConnectionFactory::class];
-
-        yield ['kafka:', RdKafkaConnectionFactory::class];
-
-        yield ['redis:', RedisConnectionFactory::class];
-
-        yield ['stomp:', StompConnectionFactory::class];
-
-        yield ['sqs:', SqsConnectionFactory::class];
-
-        yield ['gps:', GpsConnectionFactory::class];
-    }
-}
diff --git a/Tests/Functions/DsnToContextFunctionTest.php b/Tests/Functions/DsnToContextFunctionTest.php
deleted file mode 100644
index 2a8bc83..0000000
--- a/Tests/Functions/DsnToContextFunctionTest.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Functions;
-
-use Enqueue\AmqpExt\AmqpContext;
-use Enqueue\Fs\FsContext;
-use Enqueue\Gps\GpsContext;
-use Enqueue\Null\NullContext;
-use Enqueue\Redis\RedisContext;
-use Enqueue\Sqs\SqsContext;
-use Enqueue\Stomp\StompContext;
-use PHPUnit\Framework\TestCase;
-
-class DsnToContextFunctionTest extends TestCase
-{
-    public function testThrowIfDsnEmpty()
-    {
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The scheme could not be parsed from DSN ""');
-
-        \Enqueue\dsn_to_context('');
-    }
-
-    public function testThrowIfDsnMissingScheme()
-    {
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The scheme could not be parsed from DSN "dsnMissingScheme"');
-
-        \Enqueue\dsn_to_context('dsnMissingScheme');
-    }
-
-    public function testThrowIfDsnNotSupported()
-    {
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The scheme "http" is not supported. Supported "file", "amqp+ext"');
-
-        \Enqueue\dsn_to_context('http://schemeNotSupported');
-    }
-
-    /**
-     * @dataProvider provideDSNs
-     *
-     * @param mixed $dsn
-     * @param mixed $expectedFactoryClass
-     */
-    public function testReturnsExpectedFactoryInstance($dsn, $expectedFactoryClass)
-    {
-        $factory = \Enqueue\dsn_to_context($dsn);
-
-        $this->assertInstanceOf($expectedFactoryClass, $factory);
-    }
-
-    public static function provideDSNs()
-    {
-        yield ['amqp:', AmqpContext::class];
-
-        yield ['amqp://user:pass@foo/vhost', AmqpContext::class];
-
-        yield ['file:', FsContext::class];
-
-        yield ['file://'.sys_get_temp_dir(), FsContext::class];
-
-        yield ['null:', NullContext::class];
-
-        yield ['redis:', RedisContext::class];
-
-        yield ['stomp:', StompContext::class];
-
-        yield ['sqs:', SqsContext::class];
-
-        yield ['gps:', GpsContext::class];
-    }
-}
diff --git a/Tests/Mocks/CustomPrepareBodyClientExtension.php b/Tests/Mocks/CustomPrepareBodyClientExtension.php
new file mode 100644
index 0000000..dd0e1a6
--- /dev/null
+++ b/Tests/Mocks/CustomPrepareBodyClientExtension.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Enqueue\Tests\Mocks;
+
+use Enqueue\Client\PreSend;
+use Enqueue\Client\PreSendCommandExtensionInterface;
+use Enqueue\Client\PreSendEventExtensionInterface;
+
+class CustomPrepareBodyClientExtension implements PreSendEventExtensionInterface, PreSendCommandExtensionInterface
+{
+    public function onPreSendCommand(PreSend $context): void
+    {
+        $context->getMessage()->setBody('theCommandBodySerializedByCustomExtension');
+    }
+
+    public function onPreSendEvent(PreSend $context): void
+    {
+        $context->getMessage()->setBody('theEventBodySerializedByCustomExtension');
+    }
+}
diff --git a/Tests/Mocks/JsonSerializableObject.php b/Tests/Mocks/JsonSerializableObject.php
new file mode 100644
index 0000000..84885c3
--- /dev/null
+++ b/Tests/Mocks/JsonSerializableObject.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Enqueue\Tests\Mocks;
+
+class JsonSerializableObject implements \JsonSerializable
+{
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize(): array
+    {
+        return ['foo' => 'fooVal'];
+    }
+}
diff --git a/Tests/ResourcesTest.php b/Tests/ResourcesTest.php
new file mode 100644
index 0000000..ec713fd
--- /dev/null
+++ b/Tests/ResourcesTest.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace Enqueue\Tests;
+
+use Enqueue\Redis\RedisConnectionFactory;
+use Enqueue\Resources;
+use Enqueue\Wamp\WampConnectionFactory;
+use Interop\Queue\ConnectionFactory;
+use PHPUnit\Framework\TestCase;
+
+class ResourcesTest extends TestCase
+{
+    public function testShouldBeFinal()
+    {
+        $rc = new \ReflectionClass(Resources::class);
+
+        $this->assertTrue($rc->isFinal());
+    }
+
+    public function testShouldConstructorBePrivate()
+    {
+        $rc = new \ReflectionClass(Resources::class);
+
+        $this->assertTrue($rc->getConstructor()->isPrivate());
+    }
+
+    public function testShouldGetAvailableConnectionsInExpectedFormat()
+    {
+        $availableConnections = Resources::getAvailableConnections();
+
+        self::assertIsArray($availableConnections);
+        $this->assertArrayHasKey(RedisConnectionFactory::class, $availableConnections);
+
+        $connectionInfo = $availableConnections[RedisConnectionFactory::class];
+        $this->assertArrayHasKey('schemes', $connectionInfo);
+        $this->assertSame(['redis', 'rediss'], $connectionInfo['schemes']);
+
+        $this->assertArrayHasKey('supportedSchemeExtensions', $connectionInfo);
+        $this->assertSame(['predis', 'phpredis'], $connectionInfo['supportedSchemeExtensions']);
+
+        $this->assertArrayHasKey('package', $connectionInfo);
+        $this->assertSame('enqueue/redis', $connectionInfo['package']);
+    }
+
+    public function testShouldGetKnownConnectionsInExpectedFormat()
+    {
+        $availableConnections = Resources::getKnownConnections();
+
+        self::assertIsArray($availableConnections);
+        $this->assertArrayHasKey(RedisConnectionFactory::class, $availableConnections);
+
+        $connectionInfo = $availableConnections[RedisConnectionFactory::class];
+        $this->assertArrayHasKey('schemes', $connectionInfo);
+        $this->assertSame(['redis', 'rediss'], $connectionInfo['schemes']);
+
+        $this->assertArrayHasKey('supportedSchemeExtensions', $connectionInfo);
+        $this->assertSame(['predis', 'phpredis'], $connectionInfo['supportedSchemeExtensions']);
+
+        $this->assertArrayHasKey('package', $connectionInfo);
+        $this->assertSame('enqueue/redis', $connectionInfo['package']);
+    }
+
+    public function testThrowsIfConnectionClassNotImplementConnectionFactoryInterfaceOnAddConnection()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The connection factory class "stdClass" must implement "Interop\Queue\ConnectionFactory" interface.');
+
+        Resources::addConnection(\stdClass::class, [], [], 'foo');
+    }
+
+    public function testThrowsIfNoSchemesProvidedOnAddConnection()
+    {
+        $connectionClass = $this->getMockClass(ConnectionFactory::class);
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Schemes could not be empty.');
+
+        Resources::addConnection($connectionClass, [], [], 'foo');
+    }
+
+    public function testThrowsIfNoPackageProvidedOnAddConnection()
+    {
+        $connectionClass = $this->getMockClass(ConnectionFactory::class);
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Package name could not be empty.');
+
+        Resources::addConnection($connectionClass, ['foo'], [], '');
+    }
+
+    public function testShouldAllowRegisterConnectionThatIsNotInstalled()
+    {
+        Resources::addConnection('theConnectionClass', ['foo'], [], 'foo');
+
+        $knownConnections = Resources::getKnownConnections();
+        self::assertIsArray($knownConnections);
+        $this->assertArrayHasKey('theConnectionClass', $knownConnections);
+
+        $availableConnections = Resources::getAvailableConnections();
+
+        self::assertIsArray($availableConnections);
+        $this->assertArrayNotHasKey('theConnectionClass', $availableConnections);
+    }
+
+    public function testShouldAllowGetPreviouslyRegisteredConnection()
+    {
+        $connectionClass = $this->getMockClass(ConnectionFactory::class);
+
+        Resources::addConnection(
+            $connectionClass,
+            ['fooscheme', 'barscheme'],
+            ['fooextension', 'barextension'],
+            'foo/bar'
+        );
+
+        $availableConnections = Resources::getAvailableConnections();
+
+        self::assertIsArray($availableConnections);
+        $this->assertArrayHasKey($connectionClass, $availableConnections);
+
+        $connectionInfo = $availableConnections[$connectionClass];
+        $this->assertArrayHasKey('schemes', $connectionInfo);
+        $this->assertSame(['fooscheme', 'barscheme'], $connectionInfo['schemes']);
+
+        $this->assertArrayHasKey('supportedSchemeExtensions', $connectionInfo);
+        $this->assertSame(['fooextension', 'barextension'], $connectionInfo['supportedSchemeExtensions']);
+
+        $this->assertArrayHasKey('package', $connectionInfo);
+        $this->assertSame('foo/bar', $connectionInfo['package']);
+    }
+
+    public function testShouldHaveRegisteredWampConfiguration()
+    {
+        $availableConnections = Resources::getKnownConnections();
+
+        self::assertIsArray($availableConnections);
+        $this->assertArrayHasKey(WampConnectionFactory::class, $availableConnections);
+
+        $connectionInfo = $availableConnections[WampConnectionFactory::class];
+        $this->assertArrayHasKey('schemes', $connectionInfo);
+        $this->assertSame(['wamp', 'ws'], $connectionInfo['schemes']);
+
+        $this->assertArrayHasKey('supportedSchemeExtensions', $connectionInfo);
+        $this->assertSame([], $connectionInfo['supportedSchemeExtensions']);
+
+        $this->assertArrayHasKey('package', $connectionInfo);
+        $this->assertSame('enqueue/wamp', $connectionInfo['package']);
+    }
+}
diff --git a/Tests/Router/RecipientTest.php b/Tests/Router/RecipientTest.php
index 3f37372..57cc83e 100644
--- a/Tests/Router/RecipientTest.php
+++ b/Tests/Router/RecipientTest.php
@@ -3,26 +3,26 @@
 namespace Enqueue\Tests\Router;
 
 use Enqueue\Router\Recipient;
-use Interop\Queue\PsrDestination;
-use Interop\Queue\PsrMessage;
+use Interop\Queue\Destination;
+use Interop\Queue\Message as InteropMessage;
 use PHPUnit\Framework\TestCase;
 
 class RecipientTest extends TestCase
 {
     public function testShouldAllowGetMessageSetInConstructor()
     {
-        $message = $this->createMock(PsrMessage::class);
+        $message = $this->createMock(InteropMessage::class);
 
-        $recipient = new Recipient($this->createMock(PsrDestination::class), $message);
+        $recipient = new Recipient($this->createMock(Destination::class), $message);
 
         $this->assertSame($message, $recipient->getMessage());
     }
 
     public function testShouldAllowGetDestinationSetInConstructor()
     {
-        $destination = $this->createMock(PsrDestination::class);
+        $destination = $this->createMock(Destination::class);
 
-        $recipient = new Recipient($destination, $this->createMock(PsrMessage::class));
+        $recipient = new Recipient($destination, $this->createMock(InteropMessage::class));
 
         $this->assertSame($destination, $recipient->getDestination());
     }
diff --git a/Tests/Router/RouteRecipientListProcessorTest.php b/Tests/Router/RouteRecipientListProcessorTest.php
index b798043..ae878fc 100644
--- a/Tests/Router/RouteRecipientListProcessorTest.php
+++ b/Tests/Router/RouteRecipientListProcessorTest.php
@@ -9,9 +9,9 @@
 use Enqueue\Router\RecipientListRouterInterface;
 use Enqueue\Router\RouteRecipientListProcessor;
 use Enqueue\Test\ClassExtensionTrait;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrProcessor;
-use Interop\Queue\PsrProducer;
+use Interop\Queue\Context;
+use Interop\Queue\Processor;
+use Interop\Queue\Producer as InteropProducer;
 use PHPUnit\Framework\TestCase;
 
 class RouteRecipientListProcessorTest extends TestCase
@@ -20,12 +20,7 @@ class RouteRecipientListProcessorTest extends TestCase
 
     public function testShouldImplementProcessorInterface()
     {
-        $this->assertClassImplements(PsrProcessor::class, RouteRecipientListProcessor::class);
-    }
-
-    public function testCouldBeConstructedWithRouterAsFirstArgument()
-    {
-        new RouteRecipientListProcessor($this->createRecipientListRouterMock());
+        $this->assertClassImplements(Processor::class, RouteRecipientListProcessor::class);
     }
 
     public function testShouldProduceRecipientsMessagesAndAckOriginalMessage()
@@ -55,7 +50,7 @@ public function testShouldProduceRecipientsMessagesAndAckOriginalMessage()
             ->with($this->identicalTo($barRecipient->getDestination()), $this->identicalTo($barRecipient->getMessage()))
         ;
 
-        $sessionMock = $this->createPsrContextMock();
+        $sessionMock = $this->createContextMock();
         $sessionMock
             ->expects($this->once())
             ->method('createProducer')
@@ -70,23 +65,23 @@ public function testShouldProduceRecipientsMessagesAndAckOriginalMessage()
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrProducer
+     * @return \PHPUnit\Framework\MockObject\MockObject|InteropProducer
      */
     protected function createProducerMock()
     {
-        return $this->createMock(PsrProducer::class);
+        return $this->createMock(InteropProducer::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext
+     * @return \PHPUnit\Framework\MockObject\MockObject|Context
      */
-    protected function createPsrContextMock()
+    protected function createContextMock()
     {
-        return $this->createMock(PsrContext::class);
+        return $this->createMock(Context::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|RecipientListRouterInterface
+     * @return \PHPUnit\Framework\MockObject\MockObject|RecipientListRouterInterface
      */
     protected function createRecipientListRouterMock()
     {
diff --git a/Tests/Rpc/PromiseTest.php b/Tests/Rpc/PromiseTest.php
index b2cb42a..6762149 100644
--- a/Tests/Rpc/PromiseTest.php
+++ b/Tests/Rpc/PromiseTest.php
@@ -31,10 +31,10 @@ public function testOnReceiveShouldCallReceiveCallBack()
         $receiveInvoked = false;
         $receivePromise = null;
         $receiveTimeout = null;
-        $receivecb = function ($promise, $timout) use (&$receiveInvoked, &$receivePromise, &$receiveTimeout) {
+        $receivecb = function ($promise, $timeout) use (&$receiveInvoked, &$receivePromise, &$receiveTimeout) {
             $receiveInvoked = true;
             $receivePromise = $promise;
-            $receiveTimeout = $timout;
+            $receiveTimeout = $timeout;
         };
 
         $promise = new Promise($receivecb, function () {}, function () {});
@@ -50,10 +50,10 @@ public function testOnReceiveShouldCallReceiveCallBackWithTimeout()
         $receiveInvoked = false;
         $receivePromise = null;
         $receiveTimeout = null;
-        $receivecb = function ($promise, $timout) use (&$receiveInvoked, &$receivePromise, &$receiveTimeout) {
+        $receivecb = function ($promise, $timeout) use (&$receiveInvoked, &$receivePromise, &$receiveTimeout) {
             $receiveInvoked = true;
             $receivePromise = $promise;
-            $receiveTimeout = $timout;
+            $receiveTimeout = $timeout;
         };
 
         $promise = new Promise($receivecb, function () {}, function () {});
@@ -122,7 +122,7 @@ public function testOnReceiveShouldThrowExceptionIfCallbackReturnNotMessageInsta
         $promise = new Promise($receivecb, function () {}, function () {});
 
         $this->expectException(\RuntimeException::class);
-        $this->expectExceptionMessage('Expected "Interop\Queue\PsrMessage" but got: "stdClass"');
+        $this->expectExceptionMessage('Expected "Interop\Queue\Message" but got: "stdClass"');
 
         $promise->receive();
     }
@@ -136,7 +136,7 @@ public function testOnReceiveNoWaitShouldThrowExceptionIfCallbackReturnNotMessag
         $promise = new Promise(function () {}, $receivecb, function () {});
 
         $this->expectException(\RuntimeException::class);
-        $this->expectExceptionMessage('Expected "Interop\Queue\PsrMessage" but got: "stdClass"');
+        $this->expectExceptionMessage('Expected "Interop\Queue\Message" but got: "stdClass"');
 
         $promise->receiveNoWait();
     }
diff --git a/Tests/Rpc/RpcClientTest.php b/Tests/Rpc/RpcClientTest.php
index ce7aa27..db4813c 100644
--- a/Tests/Rpc/RpcClientTest.php
+++ b/Tests/Rpc/RpcClientTest.php
@@ -7,18 +7,14 @@
 use Enqueue\Null\NullQueue;
 use Enqueue\Rpc\Promise;
 use Enqueue\Rpc\RpcClient;
-use Interop\Queue\PsrConsumer;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrProducer;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context;
+use Interop\Queue\Producer as InteropProducer;
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 
 class RpcClientTest extends TestCase
 {
-    public function testCouldBeConstructedWithPsrContextAsFirstArgument()
-    {
-        new RpcClient($this->createPsrContextMock());
-    }
-
     public function testShouldSetReplyToIfNotSet()
     {
         $context = new NullContext();
@@ -80,14 +76,14 @@ public function testShouldProduceMessageToQueue()
         $message->setCorrelationId('theCorrelationId');
         $message->setReplyTo('theReplyTo');
 
-        $producer = $this->createPsrProducerMock();
+        $producer = $this->createInteropProducerMock();
         $producer
             ->expects($this->once())
             ->method('send')
             ->with($this->identicalTo($queue), $this->identicalTo($message))
         ;
 
-        $context = $this->createPsrContextMock();
+        $context = $this->createContextMock();
         $context
             ->expects($this->once())
             ->method('createProducer')
@@ -110,7 +106,7 @@ public function testShouldReceiveMessageAndAckMessageIfCorrelationEquals()
         $receivedMessage = new NullMessage();
         $receivedMessage->setCorrelationId('theCorrelationId');
 
-        $consumer = $this->createPsrConsumerMock();
+        $consumer = $this->createConsumerMock();
         $consumer
             ->expects($this->once())
             ->method('receive')
@@ -127,11 +123,11 @@ public function testShouldReceiveMessageAndAckMessageIfCorrelationEquals()
             ->method('reject')
         ;
 
-        $context = $this->createPsrContextMock();
+        $context = $this->createContextMock();
         $context
             ->expects($this->once())
             ->method('createProducer')
-            ->willReturn($this->createPsrProducerMock())
+            ->willReturn($this->createInteropProducerMock())
         ;
         $context
             ->expects($this->atLeastOnce())
@@ -162,7 +158,7 @@ public function testShouldReceiveNoWaitMessageAndAckMessageIfCorrelationEquals()
         $receivedMessage = new NullMessage();
         $receivedMessage->setCorrelationId('theCorrelationId');
 
-        $consumer = $this->createPsrConsumerMock();
+        $consumer = $this->createConsumerMock();
         $consumer
             ->expects($this->once())
             ->method('receiveNoWait')
@@ -178,11 +174,11 @@ public function testShouldReceiveNoWaitMessageAndAckMessageIfCorrelationEquals()
             ->method('reject')
         ;
 
-        $context = $this->createPsrContextMock();
+        $context = $this->createContextMock();
         $context
             ->expects($this->once())
             ->method('createProducer')
-            ->willReturn($this->createPsrProducerMock())
+            ->willReturn($this->createInteropProducerMock())
         ;
         $context
             ->expects($this->atLeastOnce())
@@ -213,14 +209,14 @@ public function testShouldDeleteQueueAfterReceiveIfDeleteReplyQueueIsTrue()
         $receivedMessage = new NullMessage();
         $receivedMessage->setCorrelationId('theCorrelationId');
 
-        $consumer = $this->createPsrConsumerMock();
+        $consumer = $this->createConsumerMock();
         $consumer
             ->expects($this->once())
             ->method('receive')
             ->willReturn($receivedMessage)
         ;
 
-        $context = $this->getMockBuilder(PsrContext::class)
+        $context = $this->getMockBuilder(Context::class)
             ->disableOriginalConstructor()
             ->setMethods(['deleteQueue'])
             ->getMockForAbstractClass()
@@ -229,7 +225,7 @@ public function testShouldDeleteQueueAfterReceiveIfDeleteReplyQueueIsTrue()
         $context
             ->expects($this->once())
             ->method('createProducer')
-            ->willReturn($this->createPsrProducerMock())
+            ->willReturn($this->createInteropProducerMock())
         ;
         $context
             ->expects($this->atLeastOnce())
@@ -267,18 +263,18 @@ public function testShouldNotCallDeleteQueueIfDeleteReplyQueueIsTrueButContextHa
         $receivedMessage = new NullMessage();
         $receivedMessage->setCorrelationId('theCorrelationId');
 
-        $consumer = $this->createPsrConsumerMock();
+        $consumer = $this->createConsumerMock();
         $consumer
             ->expects($this->once())
             ->method('receive')
             ->willReturn($receivedMessage)
         ;
 
-        $context = $this->createPsrContextMock();
+        $context = $this->createContextMock();
         $context
             ->expects($this->once())
             ->method('createProducer')
-            ->willReturn($this->createPsrProducerMock())
+            ->willReturn($this->createInteropProducerMock())
         ;
         $context
             ->expects($this->once())
@@ -329,26 +325,26 @@ public function testShouldDoSyncCall()
     }
 
     /**
-     * @return PsrContext|\PHPUnit_Framework_MockObject_MockObject|PsrProducer
+     * @return Context|MockObject|InteropProducer
      */
-    private function createPsrProducerMock()
+    private function createInteropProducerMock()
     {
-        return $this->createMock(PsrProducer::class);
+        return $this->createMock(InteropProducer::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrConsumer
+     * @return MockObject|Consumer
      */
-    private function createPsrConsumerMock()
+    private function createConsumerMock()
     {
-        return $this->createMock(PsrConsumer::class);
+        return $this->createMock(Consumer::class);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext
+     * @return MockObject|Context
      */
-    private function createPsrContextMock()
+    private function createContextMock()
     {
-        return $this->createMock(PsrContext::class);
+        return $this->createMock(Context::class);
     }
 }
diff --git a/Tests/Symfony/Client/ConsumeCommandTest.php b/Tests/Symfony/Client/ConsumeCommandTest.php
new file mode 100644
index 0000000..3758ca9
--- /dev/null
+++ b/Tests/Symfony/Client/ConsumeCommandTest.php
@@ -0,0 +1,703 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\DelegateProcessor;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Consumption\ChainExtension;
+use Enqueue\Consumption\Context\Start;
+use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\QueueConsumer;
+use Enqueue\Consumption\QueueConsumerInterface;
+use Enqueue\Container\Container;
+use Enqueue\Null\NullQueue;
+use Enqueue\Symfony\Client\ConsumeCommand;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context as InteropContext;
+use Interop\Queue\Exception\SubscriptionConsumerNotSupportedException;
+use Interop\Queue\Queue;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Tester\CommandTester;
+
+class ConsumeCommandTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeSubClassOfCommand()
+    {
+        $this->assertClassExtends(Command::class, ConsumeCommand::class);
+    }
+
+    public function testShouldNotBeFinal()
+    {
+        $this->assertClassNotFinal(ConsumeCommand::class);
+    }
+
+    public function testShouldHaveAsCommandAttributeWithCommandName()
+    {
+        $commandClass = ConsumeCommand::class;
+
+        $reflectionClass = new \ReflectionClass($commandClass);
+
+        $attributes = $reflectionClass->getAttributes(AsCommand::class);
+
+        $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.');
+
+        // Get the first attribute instance (assuming there is only one AsCommand attribute)
+        $asCommandAttribute = $attributes[0];
+
+        // Verify the 'name' parameter value
+        $attributeInstance = $asCommandAttribute->newInstance();
+        $this->assertEquals('enqueue:consume', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.');
+    }
+
+    public function testShouldHaveExpectedOptions()
+    {
+        $command = new ConsumeCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $options = $command->getDefinition()->getOptions();
+
+        $this->assertCount(9, $options);
+        $this->assertArrayHasKey('memory-limit', $options);
+        $this->assertArrayHasKey('message-limit', $options);
+        $this->assertArrayHasKey('time-limit', $options);
+        $this->assertArrayHasKey('receive-timeout', $options);
+        $this->assertArrayHasKey('niceness', $options);
+        $this->assertArrayHasKey('client', $options);
+        $this->assertArrayHasKey('logger', $options);
+        $this->assertArrayHasKey('skip', $options);
+        $this->assertArrayHasKey('setup-broker', $options);
+    }
+
+    public function testShouldHaveExpectedAttributes()
+    {
+        $command = new ConsumeCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $arguments = $command->getDefinition()->getArguments();
+
+        $this->assertCount(1, $arguments);
+        $this->assertArrayHasKey('client-queue-names', $arguments);
+    }
+
+    public function testShouldBindDefaultQueueOnly()
+    {
+        $queue = new NullQueue('');
+
+        $routeCollection = new RouteCollection([]);
+
+        $processor = $this->createDelegateProcessorMock();
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->once())
+            ->method('bind')
+            ->with($this->identicalTo($queue), $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->once())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with('default', true)
+            ->willReturn($queue)
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.client.default.queue_consumer' => $consumer,
+            'enqueue.client.default.driver' => $driver,
+            'enqueue.client.default.delegate_processor' => $processor,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([]);
+    }
+
+    public function testShouldUseRequestedClient()
+    {
+        $defaultProcessor = $this->createDelegateProcessorMock();
+
+        $defaultConsumer = $this->createQueueConsumerMock();
+        $defaultConsumer
+            ->expects($this->never())
+            ->method('bind')
+        ;
+        $defaultConsumer
+            ->expects($this->never())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $defaultDriver = $this->createDriverStub(new RouteCollection([]));
+        $defaultDriver
+            ->expects($this->never())
+            ->method('createQueue')
+        ;
+
+        $queue = new NullQueue('');
+
+        $routeCollection = new RouteCollection([]);
+
+        $fooProcessor = $this->createDelegateProcessorMock();
+
+        $fooConsumer = $this->createQueueConsumerMock();
+        $fooConsumer
+            ->expects($this->once())
+            ->method('bind')
+            ->with($this->identicalTo($queue), $this->identicalTo($fooProcessor))
+        ;
+        $fooConsumer
+            ->expects($this->once())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $fooDriver = $this->createDriverStub($routeCollection);
+        $fooDriver
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with('default', true)
+            ->willReturn($queue)
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.client.default.queue_consumer' => $defaultConsumer,
+            'enqueue.client.default.driver' => $defaultDriver,
+            'enqueue.client.default.delegate_processor' => $defaultProcessor,
+            'enqueue.client.foo.queue_consumer' => $fooConsumer,
+            'enqueue.client.foo.driver' => $fooDriver,
+            'enqueue.client.foo.delegate_processor' => $fooProcessor,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            '--client' => 'foo',
+        ]);
+    }
+
+    public function testThrowIfNotDefinedClientRequested()
+    {
+        $routeCollection = new RouteCollection([]);
+
+        $processor = $this->createDelegateProcessorMock();
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->never())
+            ->method('bind')
+        ;
+        $consumer
+            ->expects($this->never())
+            ->method('consume')
+        ;
+
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->never())
+            ->method('createQueue')
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.client.default.queue_consumer' => $consumer,
+            'enqueue.client.default.driver' => $driver,
+            'enqueue.client.default.delegate_processor' => $processor,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Client "not-defined" is not supported.');
+        $tester->execute([
+            '--client' => 'not-defined',
+        ]);
+    }
+
+    public function testShouldBindDefaultQueueIfRouteUseDifferentQueue()
+    {
+        $queue = new NullQueue('');
+
+        $routeCollection = new RouteCollection([
+            new Route('topic', Route::TOPIC, 'processor'),
+        ]);
+
+        $processor = $this->createDelegateProcessorMock();
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->once())
+            ->method('bind')
+            ->with($this->identicalTo($queue), $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->once())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with('default', true)
+            ->willReturn($queue)
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.client.default.queue_consumer' => $consumer,
+            'enqueue.client.default.driver' => $driver,
+            'enqueue.client.default.delegate_processor' => $processor,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([]);
+    }
+
+    public function testShouldBindCustomExecuteConsumptionAndUseCustomClientDestinationName()
+    {
+        $defaultQueue = new NullQueue('');
+        $customQueue = new NullQueue('');
+
+        $routeCollection = new RouteCollection([
+            new Route('topic', Route::TOPIC, 'processor', ['queue' => 'custom']),
+        ]);
+
+        $processor = $this->createDelegateProcessorMock();
+
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->at(3))
+            ->method('createQueue')
+            ->with('default', true)
+            ->willReturn($defaultQueue)
+        ;
+        $driver
+            ->expects($this->at(4))
+            ->method('createQueue')
+            ->with('custom', true)
+            ->willReturn($customQueue)
+        ;
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->at(0))
+            ->method('bind')
+            ->with($this->identicalTo($defaultQueue), $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->at(1))
+            ->method('bind')
+            ->with($this->identicalTo($customQueue), $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->at(2))
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.client.default.queue_consumer' => $consumer,
+            'enqueue.client.default.driver' => $driver,
+            'enqueue.client.default.delegate_processor' => $processor,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([]);
+    }
+
+    public function testShouldBindUserProvidedQueues()
+    {
+        $queue = new NullQueue('');
+
+        $routeCollection = new RouteCollection([
+            new Route('topic', Route::TOPIC, 'processor', ['queue' => 'custom']),
+            new Route('topic', Route::TOPIC, 'processor', ['queue' => 'non-default-queue']),
+        ]);
+
+        $processor = $this->createDelegateProcessorMock();
+
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with('non-default-queue', true)
+            ->willReturn($queue)
+        ;
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->once())
+            ->method('bind')
+            ->with($this->identicalTo($queue), $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->once())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.client.default.queue_consumer' => $consumer,
+            'enqueue.client.default.driver' => $driver,
+            'enqueue.client.default.delegate_processor' => $processor,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            'client-queue-names' => ['non-default-queue'],
+        ]);
+    }
+
+    public function testShouldBindNotPrefixedQueue()
+    {
+        $queue = new NullQueue('');
+
+        $routeCollection = new RouteCollection([
+            new Route('topic', Route::TOPIC, 'processor', ['queue' => 'non-prefixed-queue', 'prefix_queue' => false]),
+        ]);
+
+        $processor = $this->createDelegateProcessorMock();
+
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with('non-prefixed-queue', false)
+            ->willReturn($queue)
+        ;
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->once())
+            ->method('bind')
+            ->with($this->identicalTo($queue), $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->once())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.client.default.queue_consumer' => $consumer,
+            'enqueue.client.default.driver' => $driver,
+            'enqueue.client.default.delegate_processor' => $processor,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            'client-queue-names' => ['non-prefixed-queue'],
+        ]);
+    }
+
+    public function testShouldBindQueuesOnlyOnce()
+    {
+        $defaultQueue = new NullQueue('');
+        $customQueue = new NullQueue('');
+
+        $routeCollection = new RouteCollection([
+            new Route('fooTopic', Route::TOPIC, 'processor', ['queue' => 'custom']),
+            new Route('barTopic', Route::TOPIC, 'processor', ['queue' => 'custom']),
+            new Route('ololoTopic', Route::TOPIC, 'processor', []),
+        ]);
+
+        $processor = $this->createDelegateProcessorMock();
+
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->at(3))
+            ->method('createQueue')
+            ->with('default', true)
+            ->willReturn($defaultQueue)
+        ;
+        $driver
+            ->expects($this->at(4))
+            ->method('createQueue', true)
+            ->with('custom')
+            ->willReturn($customQueue)
+        ;
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->at(0))
+            ->method('bind')
+            ->with($this->identicalTo($defaultQueue), $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->at(1))
+            ->method('bind')
+            ->with($this->identicalTo($customQueue), $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->at(2))
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.client.default.queue_consumer' => $consumer,
+            'enqueue.client.default.driver' => $driver,
+            'enqueue.client.default.delegate_processor' => $processor,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([]);
+    }
+
+    public function testShouldNotBindExternalRoutes()
+    {
+        $defaultQueue = new NullQueue('');
+
+        $routeCollection = new RouteCollection([
+            new Route('barTopic', Route::TOPIC, 'processor', ['queue' => null]),
+            new Route('fooTopic', Route::TOPIC, 'processor', ['queue' => 'external_queue', 'external' => true]),
+        ]);
+
+        $processor = $this->createDelegateProcessorMock();
+
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->exactly(1))
+            ->method('createQueue')
+            ->with('default', true)
+            ->willReturn($defaultQueue)
+        ;
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->exactly(1))
+            ->method('bind')
+            ->with($this->identicalTo($defaultQueue), $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->at(1))
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.client.default.queue_consumer' => $consumer,
+            'enqueue.client.default.driver' => $driver,
+            'enqueue.client.default.delegate_processor' => $processor,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([]);
+    }
+
+    public function testShouldSkipQueueConsumptionAndUseCustomClientDestinationName()
+    {
+        $queue = new NullQueue('');
+
+        $processor = $this->createDelegateProcessorMock();
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->exactly(3))
+            ->method('bind')
+        ;
+        $consumer
+            ->expects($this->once())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $routeCollection = new RouteCollection([
+            new Route('fooTopic', Route::TOPIC, 'processor', ['queue' => 'fooQueue']),
+            new Route('barTopic', Route::TOPIC, 'processor', ['queue' => 'barQueue']),
+            new Route('ololoTopic', Route::TOPIC, 'processor', ['queue' => 'ololoQueue']),
+        ]);
+
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->at(3))
+            ->method('createQueue', true)
+            ->with('default')
+            ->willReturn($queue)
+        ;
+        $driver
+            ->expects($this->at(4))
+            ->method('createQueue', true)
+            ->with('fooQueue')
+            ->willReturn($queue)
+        ;
+        $driver
+            ->expects($this->at(5))
+            ->method('createQueue', true)
+            ->with('ololoQueue')
+            ->willReturn($queue)
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.client.default.queue_consumer' => $consumer,
+            'enqueue.client.default.driver' => $driver,
+            'enqueue.client.default.delegate_processor' => $processor,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            '--skip' => ['barQueue'],
+        ]);
+    }
+
+    public function testShouldReturnExitStatusIfSet()
+    {
+        $testExitCode = 678;
+
+        $stubExtension = $this->createExtension();
+
+        $stubExtension
+            ->expects($this->once())
+            ->method('onStart')
+            ->with($this->isInstanceOf(Start::class))
+            ->willReturnCallback(function (Start $context) use ($testExitCode) {
+                $context->interruptExecution($testExitCode);
+            })
+        ;
+
+        $defaultQueue = new NullQueue('default');
+
+        $routeCollection = new RouteCollection([]);
+
+        $processor = $this->createDelegateProcessorMock();
+
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->exactly(1))
+            ->method('createQueue')
+            ->with('default', true)
+            ->willReturn($defaultQueue)
+        ;
+
+        $consumer = new QueueConsumer($this->createContextStub(), $stubExtension);
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.client.default.queue_consumer' => $consumer,
+            'enqueue.client.default.driver' => $driver,
+            'enqueue.client.default.delegate_processor' => $processor,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([]);
+
+        $this->assertEquals($testExitCode, $tester->getStatusCode());
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|DelegateProcessor
+     */
+    private function createDelegateProcessorMock()
+    {
+        return $this->createMock(DelegateProcessor::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface
+     */
+    private function createQueueConsumerMock()
+    {
+        return $this->createMock(QueueConsumerInterface::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface
+     */
+    private function createDriverStub(?RouteCollection $routeCollection = null): DriverInterface
+    {
+        $driverMock = $this->createMock(DriverInterface::class);
+        $driverMock
+            ->expects($this->any())
+            ->method('getRouteCollection')
+            ->willReturn($routeCollection ?? new RouteCollection([]))
+        ;
+
+        $driverMock
+            ->expects($this->any())
+            ->method('getConfig')
+            ->willReturn(Config::create('aPrefix', 'anApp'))
+        ;
+
+        return $driverMock;
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createContextWithoutSubscriptionConsumerMock(): InteropContext
+    {
+        $contextMock = $this->createMock(InteropContext::class);
+        $contextMock
+            ->expects($this->any())
+            ->method('createSubscriptionConsumer')
+            ->willThrowException(SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt())
+        ;
+
+        return $contextMock;
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|InteropContext
+     */
+    private function createContextStub(?Consumer $consumer = null): InteropContext
+    {
+        $context = $this->createContextWithoutSubscriptionConsumerMock();
+        $context
+            ->expects($this->any())
+            ->method('createQueue')
+            ->willReturnCallback(function (string $queueName) {
+                return new NullQueue($queueName);
+            })
+        ;
+        $context
+            ->expects($this->any())
+            ->method('createConsumer')
+            ->willReturnCallback(function (Queue $queue) use ($consumer) {
+                return $consumer ?: $this->createConsumerStub($queue);
+            })
+        ;
+
+        return $context;
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|ExtensionInterface
+     */
+    private function createExtension()
+    {
+        return $this->createMock(ExtensionInterface::class);
+    }
+
+    /**
+     * @param mixed|null $queue
+     *
+     * @return \PHPUnit\Framework\MockObject\MockObject|Consumer
+     */
+    private function createConsumerStub($queue = null): Consumer
+    {
+        if (null === $queue) {
+            $queue = 'queue';
+        }
+        if (is_string($queue)) {
+            $queue = new NullQueue($queue);
+        }
+
+        $consumerMock = $this->createMock(Consumer::class);
+        $consumerMock
+            ->expects($this->any())
+            ->method('getQueue')
+            ->willReturn($queue)
+        ;
+
+        return $consumerMock;
+    }
+}
diff --git a/Tests/Symfony/Client/ConsumeMessagesCommandTest.php b/Tests/Symfony/Client/ConsumeMessagesCommandTest.php
deleted file mode 100644
index b6017ab..0000000
--- a/Tests/Symfony/Client/ConsumeMessagesCommandTest.php
+++ /dev/null
@@ -1,224 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Symfony\Client;
-
-use Enqueue\Client\Config;
-use Enqueue\Client\DelegateProcessor;
-use Enqueue\Client\DriverInterface;
-use Enqueue\Client\Meta\QueueMetaRegistry;
-use Enqueue\Consumption\ChainExtension;
-use Enqueue\Consumption\QueueConsumer;
-use Enqueue\Null\NullQueue;
-use Enqueue\Symfony\Client\ConsumeMessagesCommand;
-use Interop\Queue\PsrContext;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Console\Tester\CommandTester;
-
-class ConsumeMessagesCommandTest extends TestCase
-{
-    public function testCouldBeConstructedWithRequiredAttributes()
-    {
-        new ConsumeMessagesCommand(
-            $this->createQueueConsumerMock(),
-            $this->createDelegateProcessorMock(),
-            $this->createQueueMetaRegistry([]),
-            $this->createDriverMock()
-        );
-    }
-
-    public function testShouldHaveCommandName()
-    {
-        $command = new ConsumeMessagesCommand(
-            $this->createQueueConsumerMock(),
-            $this->createDelegateProcessorMock(),
-            $this->createQueueMetaRegistry([]),
-            $this->createDriverMock()
-        );
-
-        $this->assertEquals('enqueue:consume', $command->getName());
-    }
-
-    public function testShouldHaveCommandAliases()
-    {
-        $command = new ConsumeMessagesCommand(
-            $this->createQueueConsumerMock(),
-            $this->createDelegateProcessorMock(),
-            $this->createQueueMetaRegistry([]),
-            $this->createDriverMock()
-        );
-
-        $this->assertEquals(['enq:c'], $command->getAliases());
-    }
-
-    public function testShouldHaveExpectedOptions()
-    {
-        $command = new ConsumeMessagesCommand(
-            $this->createQueueConsumerMock(),
-            $this->createDelegateProcessorMock(),
-            $this->createQueueMetaRegistry([]),
-            $this->createDriverMock()
-        );
-
-        $options = $command->getDefinition()->getOptions();
-
-        $this->assertCount(6, $options);
-        $this->assertArrayHasKey('memory-limit', $options);
-        $this->assertArrayHasKey('message-limit', $options);
-        $this->assertArrayHasKey('time-limit', $options);
-        $this->assertArrayHasKey('setup-broker', $options);
-        $this->assertArrayHasKey('idle-timeout', $options);
-        $this->assertArrayHasKey('receive-timeout', $options);
-    }
-
-    public function testShouldHaveExpectedAttributes()
-    {
-        $command = new ConsumeMessagesCommand(
-            $this->createQueueConsumerMock(),
-            $this->createDelegateProcessorMock(),
-            $this->createQueueMetaRegistry([]),
-            $this->createDriverMock()
-        );
-
-        $arguments = $command->getDefinition()->getArguments();
-
-        $this->assertCount(1, $arguments);
-        $this->assertArrayHasKey('client-queue-names', $arguments);
-    }
-
-    public function testShouldExecuteConsumptionAndUseDefaultQueueName()
-    {
-        $queue = new NullQueue('');
-
-        $processor = $this->createDelegateProcessorMock();
-
-        $context = $this->createPsrContextMock();
-        $context
-            ->expects($this->never())
-            ->method('close')
-        ;
-
-        $consumer = $this->createQueueConsumerMock();
-        $consumer
-            ->expects($this->once())
-            ->method('bind')
-            ->with($this->identicalTo($queue), $this->identicalTo($processor))
-        ;
-        $consumer
-            ->expects($this->once())
-            ->method('consume')
-            ->with($this->isInstanceOf(ChainExtension::class))
-        ;
-
-        $queueMetaRegistry = $this->createQueueMetaRegistry([
-            'default' => [],
-        ]);
-
-        $driver = $this->createDriverMock();
-        $driver
-            ->expects($this->once())
-            ->method('createQueue')
-            ->with('default')
-            ->willReturn($queue)
-        ;
-
-        $command = new ConsumeMessagesCommand($consumer, $processor, $queueMetaRegistry, $driver);
-
-        $tester = new CommandTester($command);
-        $tester->execute([]);
-    }
-
-    public function testShouldExecuteConsumptionAndUseCustomClientDestinationName()
-    {
-        $queue = new NullQueue('');
-
-        $processor = $this->createDelegateProcessorMock();
-
-        $context = $this->createPsrContextMock();
-        $context
-            ->expects($this->never())
-            ->method('close')
-        ;
-
-        $consumer = $this->createQueueConsumerMock();
-        $consumer
-            ->expects($this->once())
-            ->method('bind')
-            ->with($this->identicalTo($queue), $this->identicalTo($processor))
-        ;
-        $consumer
-            ->expects($this->once())
-            ->method('consume')
-            ->with($this->isInstanceOf(ChainExtension::class))
-        ;
-
-        $queueMetaRegistry = $this->createQueueMetaRegistry([
-            'non-default-queue' => [],
-        ]);
-
-        $driver = $this->createDriverMock();
-        $driver
-            ->expects($this->once())
-            ->method('createQueue')
-            ->with('non-default-queue')
-            ->willReturn($queue)
-        ;
-
-        $command = new ConsumeMessagesCommand($consumer, $processor, $queueMetaRegistry, $driver);
-
-        $tester = new CommandTester($command);
-        $tester->execute([
-            'client-queue-names' => ['non-default-queue'],
-        ]);
-    }
-
-    /**
-     * @param array $destinationNames
-     *
-     * @return QueueMetaRegistry
-     */
-    private function createQueueMetaRegistry(array $destinationNames)
-    {
-        $config = new Config(
-            'aPrefix',
-            'anApp',
-            'aRouterTopicName',
-            'aRouterQueueName',
-            'aDefaultQueueName',
-            'aRouterProcessorName'
-        );
-
-        return new QueueMetaRegistry($config, $destinationNames);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext
-     */
-    private function createPsrContextMock()
-    {
-        return $this->createMock(PsrContext::class);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|DelegateProcessor
-     */
-    private function createDelegateProcessorMock()
-    {
-        return $this->createMock(DelegateProcessor::class);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|QueueConsumer
-     */
-    private function createQueueConsumerMock()
-    {
-        return $this->createMock(QueueConsumer::class);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface
-     */
-    private function createDriverMock()
-    {
-        return $this->createMock(DriverInterface::class);
-    }
-}
diff --git a/Tests/Symfony/Client/ContainerAwareProcessorRegistryTest.php b/Tests/Symfony/Client/ContainerAwareProcessorRegistryTest.php
deleted file mode 100644
index 1d641d4..0000000
--- a/Tests/Symfony/Client/ContainerAwareProcessorRegistryTest.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Symfony\Client;
-
-use Enqueue\Client\ProcessorRegistryInterface;
-use Enqueue\Symfony\Client\ContainerAwareProcessorRegistry;
-use Enqueue\Test\ClassExtensionTrait;
-use Interop\Queue\PsrProcessor;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\DependencyInjection\Container;
-
-class ContainerAwareProcessorRegistryTest extends TestCase
-{
-    use ClassExtensionTrait;
-
-    public function testShouldImplementProcessorRegistryInterface()
-    {
-        $this->assertClassImplements(ProcessorRegistryInterface::class, ContainerAwareProcessorRegistry::class);
-    }
-
-    public function testCouldBeConstructedWithoutAnyArgument()
-    {
-        new ContainerAwareProcessorRegistry();
-    }
-
-    public function testShouldThrowExceptionIfProcessorIsNotSet()
-    {
-        $this->setExpectedException(
-            \LogicException::class,
-            'Processor was not found. processorName: "processor-name"'
-        );
-
-        $registry = new ContainerAwareProcessorRegistry();
-        $registry->get('processor-name');
-    }
-
-    public function testShouldThrowExceptionIfContainerIsNotSet()
-    {
-        $this->setExpectedException(\LogicException::class, 'Container was not set');
-
-        $registry = new ContainerAwareProcessorRegistry();
-        $registry->set('processor-name', 'processor-id');
-
-        $registry->get('processor-name');
-    }
-
-    public function testShouldThrowExceptionIfInstanceOfProcessorIsInvalid()
-    {
-        $this->setExpectedException(\LogicException::class, 'Container was not set');
-
-        $processor = new \stdClass();
-
-        $container = new Container();
-        $container->set('processor-id', $processor);
-
-        $registry = new ContainerAwareProcessorRegistry();
-        $registry->set('processor-name', 'processor-id');
-
-        $registry->get('processor-name');
-    }
-
-    public function testShouldReturnInstanceOfProcessor()
-    {
-        $this->setExpectedException(\LogicException::class, 'Container was not set');
-
-        $processor = $this->createProcessorMock();
-
-        $container = new Container();
-        $container->set('processor-id', $processor);
-
-        $registry = new ContainerAwareProcessorRegistry();
-        $registry->set('processor-name', 'processor-id');
-
-        $this->assertSame($processor, $registry->get('processor-name'));
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor
-     */
-    protected function createProcessorMock()
-    {
-        return $this->createMock(PsrProcessor::class);
-    }
-}
diff --git a/Tests/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPassTest.php b/Tests/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPassTest.php
new file mode 100644
index 0000000..568de64
--- /dev/null
+++ b/Tests/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPassTest.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\Route;
+use Enqueue\Symfony\Client\DependencyInjection\AnalyzeRouteCollectionPass;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+class AnalyzeRouteCollectionPassTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementCompilerPass()
+    {
+        $this->assertClassImplements(CompilerPassInterface::class, AnalyzeRouteCollectionPass::class);
+    }
+
+    public function testShouldBeFinal()
+    {
+        $this->assertClassFinal(AnalyzeRouteCollectionPass::class);
+    }
+
+    public function testThrowIfEnqueueClientsParameterNotSet()
+    {
+        $pass = new AnalyzeRouteCollectionPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.');
+        $pass->process(new ContainerBuilder());
+    }
+
+    public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo', 'bar']);
+
+        $pass = new AnalyzeRouteCollectionPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found');
+        $pass->process($container);
+    }
+
+    public function testThrowIfExclusiveCommandProcessorOnDefaultQueue()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['aName']);
+        $container->register('enqueue.client.aName.route_collection')->addArgument([
+            (new Route(
+                'aCommand',
+                Route::COMMAND,
+                'aBarProcessor',
+                ['exclusive' => true]
+            ))->toArray(),
+        ]);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The command "aCommand" processor "aBarProcessor" is exclusive but queue is not specified. Exclusive processors could not be run on a default queue.');
+        $pass = new AnalyzeRouteCollectionPass();
+
+        $pass->process($container);
+    }
+
+    public function testThrowIfTwoExclusiveCommandProcessorsWorkOnSamePrefixedQueue()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['aName']);
+        $container->register('enqueue.client.aName.route_collection')->addArgument([
+            (new Route(
+                'aFooCommand',
+                Route::COMMAND,
+                'aFooProcessor',
+                ['exclusive' => true, 'queue' => 'aQueue', 'prefix_queue' => true]
+            ))->toArray(),
+
+            (new Route(
+                'aBarCommand',
+                Route::COMMAND,
+                'aBarProcessor',
+                ['exclusive' => true, 'queue' => 'aQueue', 'prefix_queue' => true]
+            ))->toArray(),
+        ]);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The command "aBarCommand" processor "aBarProcessor" is exclusive. The queue "aQueue" already has another exclusive command processor "aFooProcessor" bound to it.');
+        $pass = new AnalyzeRouteCollectionPass();
+
+        $pass->process($container);
+    }
+
+    public function testThrowIfTwoExclusiveCommandProcessorsWorkOnSameQueue()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['aName']);
+        $container->register('enqueue.client.aName.route_collection')->addArgument([
+            (new Route(
+                'aFooCommand',
+                Route::COMMAND,
+                'aFooProcessor',
+                ['exclusive' => true, 'queue' => 'aQueue', 'prefix_queue' => false]
+            ))->toArray(),
+
+            (new Route(
+                'aBarCommand',
+                Route::COMMAND,
+                'aBarProcessor',
+                ['exclusive' => true, 'queue' => 'aQueue', 'prefix_queue' => false]
+            ))->toArray(),
+        ]);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The command "aBarCommand" processor "aBarProcessor" is exclusive. The queue "aQueue" already has another exclusive command processor "aFooProcessor" bound to it.');
+        $pass = new AnalyzeRouteCollectionPass();
+
+        $pass->process($container);
+    }
+
+    public function testThrowIfThereAreTwoQueuesWithSameNameAndOneNotPrefixed()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['aName']);
+        $container->register('enqueue.client.aName.route_collection')->addArgument([
+            (new Route(
+                'aFooCommand',
+                Route::COMMAND,
+                'aFooProcessor',
+                ['queue' => 'foo', 'prefix_queue' => false]
+            ))->toArray(),
+
+            (new Route(
+                'aBarCommand',
+                Route::COMMAND,
+                'aBarProcessor',
+                ['queue' => 'foo', 'prefix_queue' => true]
+            ))->toArray(),
+        ]);
+
+        $pass = new AnalyzeRouteCollectionPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('There are prefixed and not prefixed queue with the same name "foo". This is not allowed.');
+        $pass->process($container);
+    }
+
+    public function testThrowIfDefaultQueueNotPrefixed()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['aName']);
+        $container->register('enqueue.client.aName.route_collection')->addArgument([
+            (new Route(
+                'aFooCommand',
+                Route::COMMAND,
+                'aFooProcessor',
+                ['queue' => null, 'prefix_queue' => false]
+            ))->toArray(),
+        ]);
+
+        $pass = new AnalyzeRouteCollectionPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The default queue must be prefixed.');
+        $pass->process($container);
+    }
+}
diff --git a/Tests/Symfony/Client/DependencyInjection/BuildClientExtensionsPassTest.php b/Tests/Symfony/Client/DependencyInjection/BuildClientExtensionsPassTest.php
new file mode 100644
index 0000000..7537903
--- /dev/null
+++ b/Tests/Symfony/Client/DependencyInjection/BuildClientExtensionsPassTest.php
@@ -0,0 +1,283 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\ExtensionInterface;
+use Enqueue\Symfony\Client\DependencyInjection\BuildClientExtensionsPass;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
+
+class BuildClientExtensionsPassTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementCompilerPassInterface()
+    {
+        $this->assertClassImplements(CompilerPassInterface::class, BuildClientExtensionsPass::class);
+    }
+
+    public function testShouldBeFinal()
+    {
+        $this->assertClassFinal(BuildClientExtensionsPass::class);
+    }
+
+    public function testThrowIfEnqueueClientsParameterNotSet()
+    {
+        $pass = new BuildClientExtensionsPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.');
+        $pass->process(new ContainerBuilder());
+    }
+
+    public function testThrowsIfNoClientExtensionsServiceFoundForConfiguredTransport()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo', 'bar']);
+        $container->setParameter('enqueue.default_client', 'foo');
+
+        $pass = new BuildClientExtensionsPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Service "enqueue.client.foo.client_extensions" not found');
+        $pass->process($container);
+    }
+
+    public function testShouldRegisterClientExtension()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['aName']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.aName.client_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.client_extension', ['client' => 'aName'])
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.client_extension', ['client' => 'aName'])
+        ;
+
+        $pass = new BuildClientExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+            new Reference('aBarExtension'),
+        ], $extensions->getArgument(0));
+    }
+
+    public function testShouldIgnoreOtherClientExtensions()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['aName']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.aName.client_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.client_extension', ['client' => 'aName'])
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.client_extension', ['client' => 'anotherName'])
+        ;
+
+        $pass = new BuildClientExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+        ], $extensions->getArgument(0));
+    }
+
+    public function testShouldAddExtensionIfClientAll()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['aName']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.aName.client_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.client_extension', ['client' => 'all'])
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.client_extension', ['client' => 'anotherName'])
+        ;
+
+        $pass = new BuildClientExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+        ], $extensions->getArgument(0));
+    }
+
+    public function testShouldTreatTagsWithoutClientAsDefaultClient()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.client_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.client_extension')
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.client_extension')
+        ;
+
+        $pass = new BuildClientExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+            new Reference('aBarExtension'),
+        ], $extensions->getArgument(0));
+    }
+
+    public function testShouldOrderExtensionsByPriority()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+        $container->setDefinition('enqueue.client.foo.client_extensions', $extensions);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.client_extension', ['priority' => 6]);
+        $container->setDefinition('foo_extension', $extension);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.client_extension', ['priority' => -5]);
+        $container->setDefinition('bar_extension', $extension);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.client_extension', ['priority' => 2]);
+        $container->setDefinition('baz_extension', $extension);
+
+        $pass = new BuildClientExtensionsPass();
+        $pass->process($container);
+
+        $orderedExtensions = $extensions->getArgument(0);
+
+        $this->assertCount(3, $orderedExtensions);
+        $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[0]);
+        $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[1]);
+        $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[2]);
+    }
+
+    public function testShouldAssumePriorityZeroIfPriorityIsNotSet()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+        $container->setDefinition('enqueue.client.foo.client_extensions', $extensions);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.client_extension');
+        $container->setDefinition('foo_extension', $extension);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.client_extension', ['priority' => 1]);
+        $container->setDefinition('bar_extension', $extension);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.client_extension', ['priority' => -1]);
+        $container->setDefinition('baz_extension', $extension);
+
+        $pass = new BuildClientExtensionsPass();
+        $pass->process($container);
+
+        $orderedExtensions = $extensions->getArgument(0);
+
+        $this->assertCount(3, $orderedExtensions);
+        $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[0]);
+        $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[1]);
+        $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[2]);
+    }
+
+    public function testShouldMergeWithAddedPreviously()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([
+            'aBarExtension' => 'aBarServiceIdAddedPreviously',
+            'aOloloExtension' => 'aOloloServiceIdAddedPreviously',
+        ]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.client_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.client_extension')
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.client_extension')
+        ;
+
+        $pass = new BuildClientExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertCount(4, $extensions->getArgument(0));
+    }
+
+    public function testShouldRegisterProcessorWithMatchedNameToCorrespondingExtensions()
+    {
+        $fooExtensions = new Definition();
+        $fooExtensions->addArgument([]);
+
+        $barExtensions = new Definition();
+        $barExtensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo', 'bar']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.client_extensions', $fooExtensions);
+        $container->setDefinition('enqueue.client.bar.client_extensions', $barExtensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.client_extension', ['client' => 'foo'])
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.client_extension', ['client' => 'bar'])
+        ;
+
+        $pass = new BuildClientExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($fooExtensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+        ], $fooExtensions->getArgument(0));
+
+        self::assertIsArray($barExtensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aBarExtension'),
+        ], $barExtensions->getArgument(0));
+    }
+}
diff --git a/Tests/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPassTest.php b/Tests/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPassTest.php
new file mode 100644
index 0000000..e1ed297
--- /dev/null
+++ b/Tests/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPassTest.php
@@ -0,0 +1,459 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\CommandSubscriberInterface;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Symfony\Client\DependencyInjection\BuildCommandSubscriberRoutesPass;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Processor;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+
+class BuildCommandSubscriberRoutesPassTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementCompilerPassInterface()
+    {
+        $this->assertClassImplements(CompilerPassInterface::class, BuildCommandSubscriberRoutesPass::class);
+    }
+
+    public function testShouldBeFinal()
+    {
+        $this->assertClassFinal(BuildCommandSubscriberRoutesPass::class);
+    }
+
+    public function testThrowIfEnqueueClientsParameterNotSet()
+    {
+        $pass = new BuildCommandSubscriberRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.');
+        $pass->process(new ContainerBuilder());
+    }
+
+    public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo', 'bar']);
+        $container->setParameter('enqueue.default_client', 'baz');
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found');
+        $pass->process($container);
+    }
+
+    public function testThrowIfTaggedProcessorIsBuiltByFactory()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['aName']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->register('enqueue.client.aName.route_collection', RouteCollection::class)
+            ->addArgument([])
+        ;
+        $container->register('aProcessor', Processor::class)
+            ->setFactory('foo')
+            ->addTag('enqueue.command_subscriber')
+        ;
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The command subscriber tag could not be applied to a service created by factory.');
+        $pass->process($container);
+    }
+
+    public function testShouldRegisterProcessorWithMatchedName()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', get_class($this->createCommandSubscriberProcessor()))
+            ->addTag('enqueue.command_subscriber', ['client' => 'foo'])
+        ;
+        $container->register('aProcessor', get_class($this->createCommandSubscriberProcessor()))
+            ->addTag('enqueue.command_subscriber', ['client' => 'bar'])
+        ;
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+    }
+
+    public function testShouldRegisterProcessorWithoutNameToDefaultClient()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', get_class($this->createCommandSubscriberProcessor()))
+            ->addTag('enqueue.command_subscriber')
+        ;
+        $container->register('aProcessor', get_class($this->createCommandSubscriberProcessor()))
+            ->addTag('enqueue.command_subscriber', ['client' => 'bar'])
+        ;
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+    }
+
+    public function testShouldRegisterProcessorIfClientNameEqualsAll()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', get_class($this->createCommandSubscriberProcessor()))
+            ->addTag('enqueue.command_subscriber', ['client' => 'all'])
+        ;
+        $container->register('aProcessor', get_class($this->createCommandSubscriberProcessor()))
+            ->addTag('enqueue.command_subscriber', ['client' => 'bar'])
+        ;
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+    }
+
+    public function testShouldRegisterProcessorIfCommandsIsString()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createCommandSubscriberProcessor('fooCommand');
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.command_subscriber')
+        ;
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'fooCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aFooProcessor',
+                    'processor_service_id' => 'aFooProcessor',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    public function testThrowIfCommandSubscriberReturnsNothing()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createCommandSubscriberProcessor(null);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.command_subscriber')
+        ;
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Command subscriber must return something.');
+        $pass->process($container);
+    }
+
+    public function testShouldRegisterProcessorIfCommandsAreStrings()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createCommandSubscriberProcessor(['fooCommand', 'barCommand']);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.command_subscriber')
+        ;
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(2, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'fooCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aFooProcessor',
+                    'processor_service_id' => 'aFooProcessor',
+                ],
+                [
+                    'source' => 'barCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aFooProcessor',
+                    'processor_service_id' => 'aFooProcessor',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    public function testShouldRegisterProcessorIfParamSingleCommandArray()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createCommandSubscriberProcessor([
+            'command' => 'fooCommand',
+            'processor' => 'aCustomFooProcessorName',
+            'anOption' => 'aFooVal',
+        ]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.command_subscriber')
+        ;
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+
+        $this->assertCount(1, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'fooCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aCustomFooProcessorName',
+                    'processor_service_id' => 'aFooProcessor',
+                    'anOption' => 'aFooVal',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    public function testShouldRegisterProcessorIfCommandsAreParamArrays()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createCommandSubscriberProcessor([
+            ['command' => 'fooCommand', 'processor' => 'aCustomFooProcessorName', 'anOption' => 'aFooVal'],
+            ['command' => 'barCommand', 'processor' => 'aCustomBarProcessorName', 'anOption' => 'aBarVal'],
+        ]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.command_subscriber')
+        ;
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(2, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'fooCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aCustomFooProcessorName',
+                    'processor_service_id' => 'aFooProcessor',
+                    'anOption' => 'aFooVal',
+                ],
+                [
+                    'source' => 'barCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aCustomBarProcessorName',
+                    'processor_service_id' => 'aFooProcessor',
+                    'anOption' => 'aBarVal',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    public function testThrowIfCommandSubscriberParamsInvalid()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createCommandSubscriberProcessor(['fooBar', true]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.command_subscriber')
+        ;
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Command subscriber configuration is invalid');
+        $pass->process($container);
+    }
+
+    public function testShouldMergeExtractedRoutesWithAlreadySetInCollection()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([
+            (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(),
+            (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(),
+        ]);
+
+        $processor = $this->createCommandSubscriberProcessor(['fooCommand']);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.command_subscriber')
+        ;
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(3, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'aCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aProcessor',
+                ],
+                [
+                    'source' => 'aCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aProcessor',
+                ],
+                [
+                    'source' => 'fooCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aFooProcessor',
+                    'processor_service_id' => 'aFooProcessor',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    public function testShouldRegister08CommandProcessor()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createCommandSubscriberProcessor([
+            'processorName' => 'fooCommand',
+            'queueName' => 'a_client_queue_name',
+            'queueNameHardcoded' => true,
+            'exclusive' => true,
+            'anOption' => 'aFooVal',
+        ]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['default']);
+        $container->setParameter('enqueue.default_client', 'default');
+        $container->setDefinition('enqueue.client.default.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.command_subscriber')
+        ;
+
+        $pass = new BuildCommandSubscriberRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'fooCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aFooProcessor',
+                    'processor_service_id' => 'aFooProcessor',
+                    'anOption' => 'aFooVal',
+                    'queue' => 'a_client_queue_name',
+                    'prefix_queue' => false,
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    private function createCommandSubscriberProcessor($commandSubscriberReturns = ['aCommand'])
+    {
+        $processor = new class implements Processor, CommandSubscriberInterface {
+            public static $return;
+
+            public function process(InteropMessage $message, Context $context)
+            {
+                return self::ACK;
+            }
+
+            public static function getSubscribedCommand()
+            {
+                return static::$return;
+            }
+        };
+
+        $processor::$return = $commandSubscriberReturns;
+
+        return $processor;
+    }
+}
diff --git a/Tests/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPassTest.php b/Tests/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPassTest.php
new file mode 100644
index 0000000..c297505
--- /dev/null
+++ b/Tests/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPassTest.php
@@ -0,0 +1,283 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\ExtensionInterface;
+use Enqueue\Symfony\Client\DependencyInjection\BuildConsumptionExtensionsPass;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
+
+class BuildConsumptionExtensionsPassTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementCompilerPassInterface()
+    {
+        $this->assertClassImplements(CompilerPassInterface::class, BuildConsumptionExtensionsPass::class);
+    }
+
+    public function testShouldBeFinal()
+    {
+        $this->assertClassFinal(BuildConsumptionExtensionsPass::class);
+    }
+
+    public function testThrowIfEnqueueClientsParameterNotSet()
+    {
+        $pass = new BuildConsumptionExtensionsPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.');
+        $pass->process(new ContainerBuilder());
+    }
+
+    public function testThrowsIfNoConsumptionExtensionsServiceFoundForConfiguredTransport()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo', 'bar']);
+        $container->setParameter('enqueue.default_client', 'baz');
+
+        $pass = new BuildConsumptionExtensionsPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Service "enqueue.client.foo.consumption_extensions" not found');
+        $pass->process($container);
+    }
+
+    public function testShouldRegisterClientExtension()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.consumption_extension', ['client' => 'foo'])
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.consumption_extension', ['client' => 'foo'])
+        ;
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+            new Reference('aBarExtension'),
+        ], $extensions->getArgument(0));
+    }
+
+    public function testShouldIgnoreOtherClientExtensions()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.consumption_extension', ['client' => 'foo'])
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.consumption_extension', ['client' => 'anotherName'])
+        ;
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+        ], $extensions->getArgument(0));
+    }
+
+    public function testShouldAddExtensionIfClientAll()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.consumption_extension', ['client' => 'all'])
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.consumption_extension', ['client' => 'anotherName'])
+        ;
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+        ], $extensions->getArgument(0));
+    }
+
+    public function testShouldTreatTagsWithoutClientAsDefaultClient()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.consumption_extension')
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.consumption_extension')
+        ;
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+            new Reference('aBarExtension'),
+        ], $extensions->getArgument(0));
+    }
+
+    public function testShouldOrderExtensionsByPriority()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+        $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.consumption_extension', ['priority' => 6]);
+        $container->setDefinition('foo_extension', $extension);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.consumption_extension', ['priority' => -5]);
+        $container->setDefinition('bar_extension', $extension);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.consumption_extension', ['priority' => 2]);
+        $container->setDefinition('baz_extension', $extension);
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        $orderedExtensions = $extensions->getArgument(0);
+
+        $this->assertCount(3, $orderedExtensions);
+        $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[0]);
+        $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[1]);
+        $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[2]);
+    }
+
+    public function testShouldAssumePriorityZeroIfPriorityIsNotSet()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+        $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.consumption_extension');
+        $container->setDefinition('foo_extension', $extension);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.consumption_extension', ['priority' => 1]);
+        $container->setDefinition('bar_extension', $extension);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.consumption_extension', ['priority' => -1]);
+        $container->setDefinition('baz_extension', $extension);
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        $orderedExtensions = $extensions->getArgument(0);
+
+        $this->assertCount(3, $orderedExtensions);
+        $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[0]);
+        $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[1]);
+        $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[2]);
+    }
+
+    public function testShouldMergeWithAddedPreviously()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([
+            'aBarExtension' => 'aBarServiceIdAddedPreviously',
+            'aOloloExtension' => 'aOloloServiceIdAddedPreviously',
+        ]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.consumption_extension')
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.consumption_extension')
+        ;
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertCount(4, $extensions->getArgument(0));
+    }
+
+    public function testShouldRegisterProcessorWithMatchedNameToCorrespondingExtensions()
+    {
+        $fooExtensions = new Definition();
+        $fooExtensions->addArgument([]);
+
+        $barExtensions = new Definition();
+        $barExtensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo', 'bar']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.consumption_extensions', $fooExtensions);
+        $container->setDefinition('enqueue.client.bar.consumption_extensions', $barExtensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.consumption_extension', ['client' => 'foo'])
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.consumption_extension', ['client' => 'bar'])
+        ;
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($fooExtensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+        ], $fooExtensions->getArgument(0));
+
+        self::assertIsArray($barExtensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aBarExtension'),
+        ], $barExtensions->getArgument(0));
+    }
+}
diff --git a/Tests/Symfony/Client/DependencyInjection/BuildProcessorRegistryPassTest.php b/Tests/Symfony/Client/DependencyInjection/BuildProcessorRegistryPassTest.php
new file mode 100644
index 0000000..5c9ac48
--- /dev/null
+++ b/Tests/Symfony/Client/DependencyInjection/BuildProcessorRegistryPassTest.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\Route;
+use Enqueue\Symfony\Client\DependencyInjection\BuildProcessorRegistryPass;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
+
+class BuildProcessorRegistryPassTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementCompilerPassInterface()
+    {
+        $this->assertClassImplements(CompilerPassInterface::class, BuildProcessorRegistryPass::class);
+    }
+
+    public function testShouldBeFinal()
+    {
+        $this->assertClassFinal(BuildProcessorRegistryPass::class);
+    }
+
+    public function testThrowIfEnqueueClientsParameterNotSet()
+    {
+        $pass = new BuildProcessorRegistryPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.');
+        $pass->process(new ContainerBuilder());
+    }
+
+    public function testThrowsIfNoProcessorRegistryServiceFoundForConfiguredTransport()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo', 'bar']);
+
+        $pass = new BuildProcessorRegistryPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Service "enqueue.client.foo.processor_registry" not found');
+        $pass->process($container);
+    }
+
+    public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo', 'bar']);
+        $container->register('enqueue.client.foo.processor_registry');
+
+        $pass = new BuildProcessorRegistryPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found');
+        $pass->process($container);
+    }
+
+    public function testThrowsIfNoRouteProcessorServiceFoundForConfiguredTransport()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo', 'bar']);
+        $container->register('enqueue.client.foo.processor_registry');
+        $container->register('enqueue.client.foo.route_collection');
+
+        $pass = new BuildProcessorRegistryPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Service "enqueue.client.foo.router_processor" not found');
+        $pass->process($container);
+    }
+
+    public function testThrowIfProcessorServiceIdOptionNotSet()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['aName']);
+        $container->register('enqueue.client.aName.route_collection')->addArgument([
+            (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(),
+        ]);
+        $container->register('enqueue.client.aName.processor_registry')->addArgument([]);
+        $container->register('enqueue.client.aName.router_processor');
+
+        $pass = new BuildProcessorRegistryPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The route option "processor_service_id" is required');
+        $pass->process($container);
+    }
+
+    public function testShouldPassLocatorAsFirstArgument()
+    {
+        $registry = new Definition();
+        $registry->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['aName']);
+        $container->register('enqueue.client.aName.route_collection')->addArgument([
+            (new Route(
+                'aCommand',
+                Route::COMMAND,
+                'aBarProcessor',
+                ['processor_service_id' => 'aBarServiceId']
+            ))->toArray(),
+            (new Route(
+                'aTopic',
+                Route::TOPIC,
+                'aFooProcessor',
+                ['processor_service_id' => 'aFooServiceId']
+            ))->toArray(),
+        ]);
+        $container->setDefinition('enqueue.client.aName.processor_registry', $registry);
+        $container->register('enqueue.client.aName.router_processor');
+
+        $pass = new BuildProcessorRegistryPass();
+        $pass->process($container);
+
+        $this->assertLocatorServices($container, $registry->getArgument(0), [
+            '%enqueue.client.aName.router_processor%' => 'enqueue.client.aName.router_processor',
+            'aBarProcessor' => 'aBarServiceId',
+            'aFooProcessor' => 'aFooServiceId',
+        ]);
+    }
+
+    private function assertLocatorServices(ContainerBuilder $container, $locatorId, array $locatorServices)
+    {
+        $this->assertInstanceOf(Reference::class, $locatorId);
+        $locatorId = (string) $locatorId;
+
+        $this->assertTrue($container->hasDefinition($locatorId));
+        $this->assertMatchesRegularExpression('/\.?service_locator\..*?\.enqueue\./', $locatorId);
+
+        $match = [];
+        if (false == preg_match('/(\.?service_locator\..*?)\.enqueue\./', $locatorId, $match)) {
+            $this->fail('preg_match should not failed');
+        }
+
+        $this->assertTrue($container->hasDefinition($match[1]));
+        $locator = $container->getDefinition($match[1]);
+
+        $this->assertContainsOnly(ServiceClosureArgument::class, $locator->getArgument(0));
+        $actualServices = array_map(function (ServiceClosureArgument $value) {
+            return (string) $value->getValues()[0];
+        }, $locator->getArgument(0));
+
+        $this->assertEquals($locatorServices, $actualServices);
+    }
+}
diff --git a/Tests/Symfony/Client/DependencyInjection/BuildProcessorRoutesPassTest.php b/Tests/Symfony/Client/DependencyInjection/BuildProcessorRoutesPassTest.php
new file mode 100644
index 0000000..0351c45
--- /dev/null
+++ b/Tests/Symfony/Client/DependencyInjection/BuildProcessorRoutesPassTest.php
@@ -0,0 +1,302 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Symfony\Client\DependencyInjection\BuildProcessorRoutesPass;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+
+class BuildProcessorRoutesPassTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementCompilerPassInterface()
+    {
+        $this->assertClassImplements(CompilerPassInterface::class, BuildProcessorRoutesPass::class);
+    }
+
+    public function testShouldBeFinal()
+    {
+        $this->assertClassFinal(BuildProcessorRoutesPass::class);
+    }
+
+    public function testThrowIfEnqueueClientsParameterNotSet()
+    {
+        $pass = new BuildProcessorRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.');
+        $pass->process(new ContainerBuilder());
+    }
+
+    public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo', 'bar']);
+        $container->setParameter('enqueue.default_client', 'baz');
+
+        $pass = new BuildProcessorRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found');
+        $pass->process($container);
+    }
+
+    public function testThrowIfBothTopicAndCommandAttributesAreSet()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.processor', ['topic' => 'foo', 'command' => 'bar'])
+        ;
+
+        $pass = new BuildProcessorRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Either "topic" or "command" tag attribute must be set on service "aFooProcessor". Both are set.');
+        $pass->process($container);
+    }
+
+    public function testThrowIfNeitherTopicNorCommandAttributesAreSet()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.processor', [])
+        ;
+
+        $pass = new BuildProcessorRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Either "topic" or "command" tag attribute must be set on service "aFooProcessor". None is set.');
+        $pass->process($container);
+    }
+
+    public function testShouldRegisterProcessorWithMatchedName()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'bar');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.processor', ['client' => 'foo', 'topic' => 'foo'])
+        ;
+        $container->register('aProcessor', 'aProcessorClass')
+            ->addTag('enqueue.processor', ['client' => 'bar', 'command' => 'foo'])
+        ;
+
+        $pass = new BuildProcessorRoutesPass();
+
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+    }
+
+    public function testShouldRegisterProcessorWithoutNameToDefaultClient()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.processor', ['topic' => 'foo'])
+        ;
+        $container->register('aProcessor', 'aProcessorClass')
+            ->addTag('enqueue.processor', ['client' => 'bar', 'command' => 'foo'])
+        ;
+
+        $pass = new BuildProcessorRoutesPass();
+
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+    }
+
+    public function testShouldRegisterProcessorIfClientNameEqualsAll()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.processor', ['client' => 'all', 'topic' => 'foo'])
+        ;
+        $container->register('aProcessor', 'aProcessorClass')
+            ->addTag('enqueue.processor', ['client' => 'bar', 'command' => 'foo'])
+        ;
+
+        $pass = new BuildProcessorRoutesPass();
+
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+    }
+
+    public function testShouldRegisterAsTopicProcessor()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.processor', ['topic' => 'aTopic'])
+        ;
+
+        $pass = new BuildProcessorRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'aTopic',
+                    'source_type' => 'enqueue.client.topic_route',
+                    'processor' => 'aFooProcessor',
+                    'processor_service_id' => 'aFooProcessor',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    public function testShouldRegisterAsCommandProcessor()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.processor', ['command' => 'aCommand'])
+        ;
+
+        $pass = new BuildProcessorRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'aCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aFooProcessor',
+                    'processor_service_id' => 'aFooProcessor',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    public function testShouldRegisterWithCustomProcessorName()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.processor', ['command' => 'aCommand', 'processor' => 'customProcessorName'])
+        ;
+
+        $pass = new BuildProcessorRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'aCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'customProcessorName',
+                    'processor_service_id' => 'aFooProcessor',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    public function testShouldMergeExtractedRoutesWithAlreadySetInCollection()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([
+            (new Route('aTopic', Route::TOPIC, 'aProcessor'))->toArray(),
+            (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(),
+        ]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.processor', ['command' => 'fooCommand'])
+        ;
+
+        $pass = new BuildProcessorRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(3, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'aTopic',
+                    'source_type' => 'enqueue.client.topic_route',
+                    'processor' => 'aProcessor',
+                ],
+                [
+                    'source' => 'aCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aProcessor',
+                ],
+                [
+                    'source' => 'fooCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aFooProcessor',
+                    'processor_service_id' => 'aFooProcessor',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+}
diff --git a/Tests/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPassTest.php b/Tests/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPassTest.php
new file mode 100644
index 0000000..a954d9a
--- /dev/null
+++ b/Tests/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPassTest.php
@@ -0,0 +1,423 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client\DependencyInjection;
+
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Client\TopicSubscriberInterface;
+use Enqueue\Symfony\Client\DependencyInjection\BuildTopicSubscriberRoutesPass;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Processor;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+
+class BuildTopicSubscriberRoutesPassTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementCompilerPassInterface()
+    {
+        $this->assertClassImplements(CompilerPassInterface::class, BuildTopicSubscriberRoutesPass::class);
+    }
+
+    public function testShouldBeFinal()
+    {
+        $this->assertClassFinal(BuildTopicSubscriberRoutesPass::class);
+    }
+
+    public function testThrowIfEnqueueClientsParameterNotSet()
+    {
+        $pass = new BuildTopicSubscriberRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.');
+        $pass->process(new ContainerBuilder());
+    }
+
+    public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo', 'bar']);
+        $container->setParameter('enqueue.default_client', 'baz');
+
+        $pass = new BuildTopicSubscriberRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found');
+        $pass->process($container);
+    }
+
+    public function testThrowIfTaggedProcessorIsBuiltByFactory()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->register('enqueue.client.foo.route_collection', RouteCollection::class)
+            ->addArgument([])
+        ;
+        $container->register('aProcessor', Processor::class)
+            ->setFactory('foo')
+            ->addTag('enqueue.topic_subscriber')
+        ;
+
+        $pass = new BuildTopicSubscriberRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The topic subscriber tag could not be applied to a service created by factory.');
+        $pass->process($container);
+    }
+
+    public function testShouldRegisterProcessorWithMatchedName()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'bar');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', get_class($this->createTopicSubscriberProcessor()))
+            ->addTag('enqueue.topic_subscriber', ['client' => 'foo'])
+        ;
+        $container->register('aProcessor', get_class($this->createTopicSubscriberProcessor()))
+            ->addTag('enqueue.topic_subscriber', ['client' => 'bar'])
+        ;
+
+        $pass = new BuildTopicSubscriberRoutesPass();
+
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+    }
+
+    public function testShouldRegisterProcessorWithoutNameToDefaultClient()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', get_class($this->createTopicSubscriberProcessor()))
+            ->addTag('enqueue.topic_subscriber')
+        ;
+        $container->register('aProcessor', get_class($this->createTopicSubscriberProcessor()))
+            ->addTag('enqueue.topic_subscriber', ['client' => 'bar'])
+        ;
+
+        $pass = new BuildTopicSubscriberRoutesPass();
+
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+    }
+
+    public function testShouldRegisterProcessorIfClientNameEqualsAll()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', get_class($this->createTopicSubscriberProcessor()))
+            ->addTag('enqueue.topic_subscriber', ['client' => 'all'])
+        ;
+        $container->register('aProcessor', get_class($this->createTopicSubscriberProcessor()))
+            ->addTag('enqueue.topic_subscriber', ['client' => 'bar'])
+        ;
+
+        $pass = new BuildTopicSubscriberRoutesPass();
+
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+    }
+
+    public function testShouldRegisterProcessorIfTopicsIsString()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createTopicSubscriberProcessor('fooTopic');
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.topic_subscriber')
+        ;
+
+        $pass = new BuildTopicSubscriberRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(1, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'fooTopic',
+                    'source_type' => 'enqueue.client.topic_route',
+                    'processor' => 'aFooProcessor',
+                    'processor_service_id' => 'aFooProcessor',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    public function testThrowIfTopicSubscriberReturnsNothing()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createTopicSubscriberProcessor(null);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.topic_subscriber')
+        ;
+
+        $pass = new BuildTopicSubscriberRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Topic subscriber must return something.');
+        $pass->process($container);
+    }
+
+    public function testShouldRegisterProcessorIfTopicsAreStrings()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createTopicSubscriberProcessor(['fooTopic', 'barTopic']);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.topic_subscriber')
+        ;
+
+        $pass = new BuildTopicSubscriberRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(2, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'fooTopic',
+                    'source_type' => 'enqueue.client.topic_route',
+                    'processor' => 'aFooProcessor',
+                    'processor_service_id' => 'aFooProcessor',
+                ],
+                [
+                    'source' => 'barTopic',
+                    'source_type' => 'enqueue.client.topic_route',
+                    'processor' => 'aFooProcessor',
+                    'processor_service_id' => 'aFooProcessor',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    public function testShouldRegisterProcessorIfTopicsAreParamArrays()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createTopicSubscriberProcessor([
+            ['topic' => 'fooTopic', 'processor' => 'aCustomFooProcessorName', 'anOption' => 'aFooVal'],
+            ['topic' => 'barTopic', 'processor' => 'aCustomBarProcessorName', 'anOption' => 'aBarVal'],
+        ]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.topic_subscriber')
+        ;
+
+        $pass = new BuildTopicSubscriberRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(2, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'fooTopic',
+                    'source_type' => 'enqueue.client.topic_route',
+                    'processor' => 'aCustomFooProcessorName',
+                    'processor_service_id' => 'aFooProcessor',
+                    'anOption' => 'aFooVal',
+                ],
+                [
+                    'source' => 'barTopic',
+                    'source_type' => 'enqueue.client.topic_route',
+                    'processor' => 'aCustomBarProcessorName',
+                    'processor_service_id' => 'aFooProcessor',
+                    'anOption' => 'aBarVal',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    public function testThrowIfTopicSubscriberParamsInvalid()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createTopicSubscriberProcessor(['fooBar', true]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.topic_subscriber')
+        ;
+
+        $pass = new BuildTopicSubscriberRoutesPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Topic subscriber configuration is invalid');
+        $pass->process($container);
+    }
+
+    public function testShouldMergeExtractedRoutesWithAlreadySetInCollection()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([
+            (new Route('aTopic', Route::TOPIC, 'aProcessor'))->toArray(),
+            (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(),
+        ]);
+
+        $processor = $this->createTopicSubscriberProcessor(['fooTopic']);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['foo']);
+        $container->setParameter('enqueue.default_client', 'foo');
+        $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.topic_subscriber')
+        ;
+
+        $pass = new BuildTopicSubscriberRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(3, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'aTopic',
+                    'source_type' => 'enqueue.client.topic_route',
+                    'processor' => 'aProcessor',
+                ],
+                [
+                    'source' => 'aCommand',
+                    'source_type' => 'enqueue.client.command_route',
+                    'processor' => 'aProcessor',
+                ],
+                [
+                    'source' => 'fooTopic',
+                    'source_type' => 'enqueue.client.topic_route',
+                    'processor' => 'aFooProcessor',
+                    'processor_service_id' => 'aFooProcessor',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    public function testShouldRegister08TopicSubscriber()
+    {
+        $routeCollection = new Definition(RouteCollection::class);
+        $routeCollection->addArgument([]);
+
+        $processor = $this->createTopicSubscriberProcessor([
+            'fooTopic' => ['processorName' => 'aCustomFooProcessorName', 'queueName' => 'fooQueue', 'queueNameHardcoded' => true, 'anOption' => 'aFooVal'],
+            'barTopic' => ['processorName' => 'aCustomBarProcessorName', 'anOption' => 'aBarVal'],
+        ]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.clients', ['default']);
+        $container->setParameter('enqueue.default_client', 'default');
+        $container->setDefinition('enqueue.client.default.route_collection', $routeCollection);
+        $container->register('aFooProcessor', $processor::class)
+            ->addTag('enqueue.topic_subscriber')
+        ;
+
+        $pass = new BuildTopicSubscriberRoutesPass();
+        $pass->process($container);
+
+        self::assertIsArray($routeCollection->getArgument(0));
+        $this->assertCount(2, $routeCollection->getArgument(0));
+
+        $this->assertEquals(
+            [
+                [
+                    'source' => 'fooTopic',
+                    'source_type' => 'enqueue.client.topic_route',
+                    'processor' => 'aCustomFooProcessorName',
+                    'processor_service_id' => 'aFooProcessor',
+                    'anOption' => 'aFooVal',
+                    'queue' => 'fooQueue',
+                    'prefix_queue' => false,
+                ],
+                [
+                    'source' => 'barTopic',
+                    'source_type' => 'enqueue.client.topic_route',
+                    'processor' => 'aCustomBarProcessorName',
+                    'processor_service_id' => 'aFooProcessor',
+                    'anOption' => 'aBarVal',
+                ],
+            ],
+            $routeCollection->getArgument(0)
+        );
+    }
+
+    private function createTopicSubscriberProcessor($topicSubscriberReturns = ['aTopic'])
+    {
+        $processor = new class implements Processor, TopicSubscriberInterface {
+            public static $return;
+
+            public function process(InteropMessage $message, Context $context)
+            {
+                return self::ACK;
+            }
+
+            public static function getSubscribedTopics()
+            {
+                return static::$return;
+            }
+        };
+
+        $processor::$return = $topicSubscriberReturns;
+
+        return $processor;
+    }
+}
diff --git a/Tests/Symfony/Client/DependencyInjection/ClientFactoryTest.php b/Tests/Symfony/Client/DependencyInjection/ClientFactoryTest.php
new file mode 100644
index 0000000..9f37dff
--- /dev/null
+++ b/Tests/Symfony/Client/DependencyInjection/ClientFactoryTest.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client\DependencyInjection;
+
+use Enqueue\Symfony\Client\DependencyInjection\ClientFactory;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+
+class ClientFactoryTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeFinal()
+    {
+        $this->assertClassFinal(ClientFactory::class);
+    }
+
+    public function testThrowIfEmptyNameGivenOnConstruction()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The name could not be empty.');
+
+        new ClientFactory('');
+    }
+
+    public function testShouldCreateDriverFromDsn()
+    {
+        $container = new ContainerBuilder();
+
+        $transport = new ClientFactory('default');
+
+        $serviceId = $transport->createDriver($container, ['dsn' => 'foo://bar/baz', 'foo' => 'fooVal']);
+
+        $this->assertEquals('enqueue.client.default.driver', $serviceId);
+
+        $this->assertTrue($container->hasDefinition('enqueue.client.default.driver'));
+
+        $this->assertNotEmpty($container->getDefinition('enqueue.client.default.driver')->getFactory());
+        $this->assertEquals(
+            [new Reference('enqueue.client.default.driver_factory'), 'create'],
+            $container->getDefinition('enqueue.client.default.driver')->getFactory())
+        ;
+        $this->assertEquals(
+            [
+                new Reference('enqueue.transport.default.connection_factory'),
+                new Reference('enqueue.client.default.config'),
+                new Reference('enqueue.client.default.route_collection'),
+            ],
+            $container->getDefinition('enqueue.client.default.driver')->getArguments())
+        ;
+    }
+}
diff --git a/Tests/Symfony/Client/FlushSpoolProducerListenerTest.php b/Tests/Symfony/Client/FlushSpoolProducerListenerTest.php
index a1fe06e..539d332 100644
--- a/Tests/Symfony/Client/FlushSpoolProducerListenerTest.php
+++ b/Tests/Symfony/Client/FlushSpoolProducerListenerTest.php
@@ -23,7 +23,7 @@ public function testShouldSubscribeOnKernelTerminateEvent()
     {
         $events = FlushSpoolProducerListener::getSubscribedEvents();
 
-        $this->assertInternalType('array', $events);
+        self::assertIsArray($events);
         $this->assertArrayHasKey(KernelEvents::TERMINATE, $events);
 
         $this->assertEquals('flushMessages', $events[KernelEvents::TERMINATE]);
@@ -33,17 +33,12 @@ public function testShouldSubscribeOnConsoleTerminateEvent()
     {
         $events = FlushSpoolProducerListener::getSubscribedEvents();
 
-        $this->assertInternalType('array', $events);
+        self::assertIsArray($events);
         $this->assertArrayHasKey(ConsoleEvents::TERMINATE, $events);
 
         $this->assertEquals('flushMessages', $events[ConsoleEvents::TERMINATE]);
     }
 
-    public function testCouldBeConstructedWithSpoolProducerAsFirstArgument()
-    {
-        new FlushSpoolProducerListener($this->createSpoolProducerMock());
-    }
-
     public function testShouldFlushSpoolProducerOnFlushMessagesCall()
     {
         $producerMock = $this->createSpoolProducerMock();
@@ -58,7 +53,7 @@ public function testShouldFlushSpoolProducerOnFlushMessagesCall()
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|SpoolProducer
+     * @return \PHPUnit\Framework\MockObject\MockObject|SpoolProducer
      */
     private function createSpoolProducerMock()
     {
diff --git a/Tests/Symfony/Client/Meta/QueuesCommandTest.php b/Tests/Symfony/Client/Meta/QueuesCommandTest.php
deleted file mode 100644
index f0720c7..0000000
--- a/Tests/Symfony/Client/Meta/QueuesCommandTest.php
+++ /dev/null
@@ -1,109 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Symfony\Client\Meta;
-
-use Enqueue\Client\Meta\QueueMeta;
-use Enqueue\Client\Meta\QueueMetaRegistry;
-use Enqueue\Symfony\Client\Meta\QueuesCommand;
-use Enqueue\Test\ClassExtensionTrait;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Tester\CommandTester;
-
-class QueuesCommandTest extends TestCase
-{
-    use ClassExtensionTrait;
-
-    public function testShouldBeSubClassOfCommand()
-    {
-        $this->assertClassExtends(Command::class, QueuesCommand::class);
-    }
-
-    public function testCouldBeConstructedWithQueueMetaRegistryAsFirstArgument()
-    {
-        new QueuesCommand($this->createQueueMetaRegistryStub());
-    }
-
-    public function testShouldHaveCommandName()
-    {
-        $command = new QueuesCommand($this->createQueueMetaRegistryStub());
-
-        $this->assertEquals('enqueue:queues', $command->getName());
-    }
-
-    public function testShouldHaveCommandAliases()
-    {
-        $command = new QueuesCommand($this->createQueueMetaRegistryStub());
-
-        $this->assertEquals(['enq:m:q', 'debug:enqueue:queues'], $command->getAliases());
-    }
-
-    public function testShouldShowMessageFoundZeroDestinationsIfAnythingInRegistry()
-    {
-        $command = new QueuesCommand($this->createQueueMetaRegistryStub());
-
-        $output = $this->executeCommand($command);
-
-        $this->assertContains('Found 0 destinations', $output);
-    }
-
-    public function testShouldShowMessageFoundTwoDestinations()
-    {
-        $command = new QueuesCommand($this->createQueueMetaRegistryStub([
-            new QueueMeta('aClientName', 'aDestinationName'),
-            new QueueMeta('anotherClientName', 'anotherDestinationName'),
-        ]));
-
-        $output = $this->executeCommand($command);
-
-        $this->assertContains('Found 2 destinations', $output);
-    }
-
-    public function testShouldShowInfoAboutDestinations()
-    {
-        $command = new QueuesCommand($this->createQueueMetaRegistryStub([
-            new QueueMeta('aFooClientName', 'aFooDestinationName', ['fooSubscriber']),
-            new QueueMeta('aBarClientName', 'aBarDestinationName', ['barSubscriber']),
-        ]));
-
-        $output = $this->executeCommand($command);
-
-        $this->assertContains('aFooClientName', $output);
-        $this->assertContains('aFooDestinationName', $output);
-        $this->assertContains('fooSubscriber', $output);
-        $this->assertContains('aBarClientName', $output);
-        $this->assertContains('aBarDestinationName', $output);
-        $this->assertContains('barSubscriber', $output);
-    }
-
-    /**
-     * @param Command  $command
-     * @param string[] $arguments
-     *
-     * @return string
-     */
-    protected function executeCommand(Command $command, array $arguments = [])
-    {
-        $tester = new CommandTester($command);
-        $tester->execute($arguments);
-
-        return $tester->getDisplay();
-    }
-
-    /**
-     * @param mixed $destinations
-     *
-     * @return \PHPUnit_Framework_MockObject_MockObject|QueueMetaRegistry
-     */
-    protected function createQueueMetaRegistryStub($destinations = [])
-    {
-        $registryMock = $this->createMock(QueueMetaRegistry::class);
-        $registryMock
-            ->expects($this->any())
-            ->method('getQueuesMeta')
-            ->willReturn($destinations)
-        ;
-
-        return $registryMock;
-    }
-}
diff --git a/Tests/Symfony/Client/Meta/TopicsCommandTest.php b/Tests/Symfony/Client/Meta/TopicsCommandTest.php
deleted file mode 100644
index 4efdd2f..0000000
--- a/Tests/Symfony/Client/Meta/TopicsCommandTest.php
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Symfony\Client\Meta;
-
-use Enqueue\Client\Meta\TopicMetaRegistry;
-use Enqueue\Symfony\Client\Meta\TopicsCommand;
-use Enqueue\Test\ClassExtensionTrait;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Tester\CommandTester;
-
-class TopicsCommandTest extends TestCase
-{
-    use ClassExtensionTrait;
-
-    public function testShouldBeSubClassOfCommand()
-    {
-        $this->assertClassExtends(Command::class, TopicsCommand::class);
-    }
-
-    public function testCouldBeConstructedWithTopicMetaRegistryAsFirstArgument()
-    {
-        new TopicsCommand(new TopicMetaRegistry([]));
-    }
-
-    public function testShouldHaveCommandName()
-    {
-        $command = new TopicsCommand(new TopicMetaRegistry([]));
-
-        $this->assertEquals('enqueue:topics', $command->getName());
-    }
-
-    public function testShouldHaveCommandAliases()
-    {
-        $command = new TopicsCommand(new TopicMetaRegistry([]));
-
-        $this->assertEquals(['enq:m:t', 'debug:enqueue:topics'], $command->getAliases());
-    }
-
-    public function testShouldShowMessageFoundZeroTopicsIfAnythingInRegistry()
-    {
-        $command = new TopicsCommand(new TopicMetaRegistry([]));
-
-        $output = $this->executeCommand($command);
-
-        $this->assertContains('Found 0 topics', $output);
-    }
-
-    public function testShouldShowMessageFoundTwoTopics()
-    {
-        $command = new TopicsCommand(new TopicMetaRegistry([
-            'fooTopic' => [],
-            'barTopic' => [],
-        ]));
-
-        $output = $this->executeCommand($command);
-
-        $this->assertContains('Found 2 topics', $output);
-    }
-
-    public function testShouldShowInfoAboutTopics()
-    {
-        $command = new TopicsCommand(new TopicMetaRegistry([
-            'fooTopic' => ['description' => 'fooDescription', 'processors' => ['fooSubscriber']],
-            'barTopic' => ['description' => 'barDescription', 'processors' => ['barSubscriber']],
-        ]));
-
-        $output = $this->executeCommand($command);
-
-        $this->assertContains('fooTopic', $output);
-        $this->assertContains('fooDescription', $output);
-        $this->assertContains('fooSubscriber', $output);
-        $this->assertContains('barTopic', $output);
-        $this->assertContains('barDescription', $output);
-        $this->assertContains('barSubscriber', $output);
-    }
-
-    /**
-     * @param Command  $command
-     * @param string[] $arguments
-     *
-     * @return string
-     */
-    protected function executeCommand(Command $command, array $arguments = [])
-    {
-        $tester = new CommandTester($command);
-        $tester->execute($arguments);
-
-        return $tester->getDisplay();
-    }
-}
diff --git a/Tests/Symfony/Client/Mock/SetupBrokerExtensionCommand.php b/Tests/Symfony/Client/Mock/SetupBrokerExtensionCommand.php
index fe7a352..c217505 100644
--- a/Tests/Symfony/Client/Mock/SetupBrokerExtensionCommand.php
+++ b/Tests/Symfony/Client/Mock/SetupBrokerExtensionCommand.php
@@ -3,8 +3,8 @@
 namespace Enqueue\Tests\Symfony\Client\Mock;
 
 use Enqueue\Client\Config;
-use Enqueue\Client\Meta\QueueMetaRegistry;
-use Enqueue\Null\Client\NullDriver;
+use Enqueue\Client\Driver\GenericDriver;
+use Enqueue\Client\RouteCollection;
 use Enqueue\Null\NullContext;
 use Enqueue\Symfony\Client\SetupBrokerExtensionCommandTrait;
 use Symfony\Component\Console\Command\Command;
@@ -29,12 +29,14 @@ protected function configure()
         $this->configureSetupBrokerExtension();
     }
 
-    protected function execute(InputInterface $input, OutputInterface $output)
+    protected function execute(InputInterface $input, OutputInterface $output): int
     {
-        $this->extension = $this->getSetupBrokerExtension($input, new NullDriver(
+        $this->extension = $this->getSetupBrokerExtension($input, new GenericDriver(
             new NullContext(),
             Config::create(),
-            new QueueMetaRegistry(Config::create(), [])
+            new RouteCollection([])
         ));
+
+        return 0;
     }
 }
diff --git a/Tests/Symfony/Client/ProduceCommandTest.php b/Tests/Symfony/Client/ProduceCommandTest.php
new file mode 100644
index 0000000..daa9091
--- /dev/null
+++ b/Tests/Symfony/Client/ProduceCommandTest.php
@@ -0,0 +1,284 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client;
+
+use Enqueue\Client\Message;
+use Enqueue\Client\ProducerInterface;
+use Enqueue\Container\Container;
+use Enqueue\Symfony\Client\ProduceCommand;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Tester\CommandTester;
+
+class ProduceCommandTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeSubClassOfCommand()
+    {
+        $this->assertClassExtends(Command::class, ProduceCommand::class);
+    }
+
+    public function testShouldNotBeFinal()
+    {
+        $this->assertClassNotFinal(ProduceCommand::class);
+    }
+
+    public function testShouldHaveAsCommandAttributeWithCommandName()
+    {
+        $commandClass = ProduceCommand::class;
+
+        $reflectionClass = new \ReflectionClass($commandClass);
+
+        $attributes = $reflectionClass->getAttributes(AsCommand::class);
+
+        $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.');
+
+        // Get the first attribute instance (assuming there is only one AsCommand attribute)
+        $asCommandAttribute = $attributes[0];
+
+        // Verify the 'name' parameter value
+        $attributeInstance = $asCommandAttribute->newInstance();
+        $this->assertEquals('enqueue:produce', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.');
+    }
+
+    public function testShouldHaveExpectedOptions()
+    {
+        $command = new ProduceCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $options = $command->getDefinition()->getOptions();
+        $this->assertCount(4, $options);
+        $this->assertArrayHasKey('client', $options);
+        $this->assertArrayHasKey('topic', $options);
+        $this->assertArrayHasKey('command', $options);
+        $this->assertArrayHasKey('header', $options);
+    }
+
+    public function testShouldHaveExpectedAttributes()
+    {
+        $command = new ProduceCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $arguments = $command->getDefinition()->getArguments();
+        $this->assertCount(1, $arguments);
+
+        $this->assertArrayHasKey('message', $arguments);
+    }
+
+    public function testThrowIfNeitherTopicNorCommandOptionsAreSet()
+    {
+        $producerMock = $this->createProducerMock();
+        $producerMock
+            ->expects($this->never())
+            ->method('sendEvent')
+        ;
+        $producerMock
+            ->expects($this->never())
+            ->method('sendCommand')
+        ;
+
+        $command = new ProduceCommand(new Container([
+            'enqueue.client.default.producer' => $producerMock,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Either topic or command option should be set, none is set.');
+        $tester->execute([
+            'message' => 'theMessage',
+        ]);
+    }
+
+    public function testThrowIfBothTopicAndCommandOptionsAreSet()
+    {
+        $producerMock = $this->createProducerMock();
+        $producerMock
+            ->expects($this->never())
+            ->method('sendEvent')
+        ;
+        $producerMock
+            ->expects($this->never())
+            ->method('sendCommand')
+        ;
+
+        $command = new ProduceCommand(new Container([
+            'enqueue.client.default.producer' => $producerMock,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Either topic or command option should be set, both are set.');
+        $tester->execute([
+            'message' => 'theMessage',
+            '--topic' => 'theTopic',
+            '--command' => 'theCommand',
+        ]);
+    }
+
+    public function testShouldSendEventToDefaultTransport()
+    {
+        $header = 'Content-Type: text/plain';
+        $payload = 'theMessage';
+
+        $producerMock = $this->createProducerMock();
+        $producerMock
+            ->expects($this->once())
+            ->method('sendEvent')
+            ->with('theTopic', new Message($payload, [], [$header]))
+        ;
+        $producerMock
+            ->expects($this->never())
+            ->method('sendCommand')
+        ;
+
+        $command = new ProduceCommand(new Container([
+            'enqueue.client.default.producer' => $producerMock,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            'message' => $payload,
+            '--header' => $header,
+            '--topic' => 'theTopic',
+        ]);
+    }
+
+    public function testShouldSendCommandToDefaultTransport()
+    {
+        $producerMock = $this->createProducerMock();
+        $producerMock
+            ->expects($this->once())
+            ->method('sendCommand')
+            ->with('theCommand', 'theMessage')
+        ;
+        $producerMock
+            ->expects($this->never())
+            ->method('sendEvent')
+        ;
+
+        $command = new ProduceCommand(new Container([
+            'enqueue.client.default.producer' => $producerMock,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            'message' => 'theMessage',
+            '--command' => 'theCommand',
+        ]);
+    }
+
+    public function testShouldSendEventToFooTransport()
+    {
+        $header = 'Content-Type: text/plain';
+        $payload = 'theMessage';
+
+        $defaultProducerMock = $this->createProducerMock();
+        $defaultProducerMock
+            ->expects($this->never())
+            ->method('sendEvent')
+        ;
+        $defaultProducerMock
+            ->expects($this->never())
+            ->method('sendCommand')
+        ;
+
+        $fooProducerMock = $this->createProducerMock();
+        $fooProducerMock
+            ->expects($this->once())
+            ->method('sendEvent')
+            ->with('theTopic', new Message($payload, [], [$header]))
+        ;
+        $fooProducerMock
+            ->expects($this->never())
+            ->method('sendCommand')
+        ;
+
+        $command = new ProduceCommand(new Container([
+            'enqueue.client.default.producer' => $defaultProducerMock,
+            'enqueue.client.foo.producer' => $fooProducerMock,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            'message' => $payload,
+            '--header' => $header,
+            '--topic' => 'theTopic',
+            '--client' => 'foo',
+        ]);
+    }
+
+    public function testShouldSendCommandToFooTransport()
+    {
+        $defaultProducerMock = $this->createProducerMock();
+        $defaultProducerMock
+            ->expects($this->never())
+            ->method('sendEvent')
+        ;
+        $defaultProducerMock
+            ->expects($this->never())
+            ->method('sendCommand')
+        ;
+
+        $fooProducerMock = $this->createProducerMock();
+        $fooProducerMock
+            ->expects($this->once())
+            ->method('sendCommand')
+            ->with('theCommand', 'theMessage')
+        ;
+        $fooProducerMock
+            ->expects($this->never())
+            ->method('sendEvent')
+        ;
+
+        $command = new ProduceCommand(new Container([
+            'enqueue.client.default.producer' => $defaultProducerMock,
+            'enqueue.client.foo.producer' => $fooProducerMock,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            'message' => 'theMessage',
+            '--command' => 'theCommand',
+            '--client' => 'foo',
+        ]);
+    }
+
+    public function testThrowIfClientNotFound()
+    {
+        $defaultProducerMock = $this->createProducerMock();
+        $defaultProducerMock
+            ->expects($this->never())
+            ->method('sendEvent')
+        ;
+        $defaultProducerMock
+            ->expects($this->never())
+            ->method('sendCommand')
+        ;
+
+        $command = new ProduceCommand(new Container([
+            'enqueue.client.default.producer' => $defaultProducerMock,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Client "bar" is not supported.');
+        $tester->execute([
+            'message' => 'theMessage',
+            '--command' => 'theCommand',
+            '--client' => 'bar',
+        ]);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface
+     */
+    private function createProducerMock()
+    {
+        return $this->createMock(ProducerInterface::class);
+    }
+}
diff --git a/Tests/Symfony/Client/ProduceMessageCommandTest.php b/Tests/Symfony/Client/ProduceMessageCommandTest.php
deleted file mode 100644
index 2cd8095..0000000
--- a/Tests/Symfony/Client/ProduceMessageCommandTest.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Symfony\Client;
-
-use Enqueue\Client\ProducerInterface;
-use Enqueue\Symfony\Client\ProduceMessageCommand;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Console\Tester\CommandTester;
-
-class ProduceMessageCommandTest extends TestCase
-{
-    public function testCouldBeConstructedWithMessageProducerAsFirstArgument()
-    {
-        new ProduceMessageCommand($this->createProducerMock());
-    }
-
-    public function testShouldHaveCommandName()
-    {
-        $command = new ProduceMessageCommand($this->createProducerMock());
-
-        $this->assertEquals('enqueue:produce', $command->getName());
-    }
-
-    public function testShouldHaveCommandAliases()
-    {
-        $command = new ProduceMessageCommand($this->createProducerMock());
-
-        $this->assertEquals(['enq:p'], $command->getAliases());
-    }
-
-    public function testShouldHaveExpectedOptions()
-    {
-        $command = new ProduceMessageCommand($this->createProducerMock());
-
-        $options = $command->getDefinition()->getOptions();
-        $this->assertCount(0, $options);
-    }
-
-    public function testShouldHaveExpectedAttributes()
-    {
-        $command = new ProduceMessageCommand($this->createProducerMock());
-
-        $arguments = $command->getDefinition()->getArguments();
-        $this->assertCount(2, $arguments);
-
-        $this->assertArrayHasKey('topic', $arguments);
-        $this->assertArrayHasKey('message', $arguments);
-    }
-
-    public function testShouldExecuteConsumptionAndUseDefaultQueueName()
-    {
-        $producerMock = $this->createProducerMock();
-        $producerMock
-            ->expects($this->once())
-            ->method('sendEvent')
-            ->with('theTopic', 'theMessage')
-        ;
-
-        $command = new ProduceMessageCommand($producerMock);
-
-        $tester = new CommandTester($command);
-        $tester->execute([
-            'topic' => 'theTopic',
-            'message' => 'theMessage',
-        ]);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|ProducerInterface
-     */
-    private function createProducerMock()
-    {
-        return $this->createMock(ProducerInterface::class);
-    }
-}
diff --git a/Tests/Symfony/Client/RoutesCommandTest.php b/Tests/Symfony/Client/RoutesCommandTest.php
new file mode 100644
index 0000000..89bd7f7
--- /dev/null
+++ b/Tests/Symfony/Client/RoutesCommandTest.php
@@ -0,0 +1,366 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\Route;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Container\Container;
+use Enqueue\Symfony\Client\RoutesCommand;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Tester\CommandTester;
+
+class RoutesCommandTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeSubClassOfCommand()
+    {
+        $this->assertClassExtends(Command::class, RoutesCommand::class);
+    }
+
+    public function testShouldNotBeFinal()
+    {
+        $this->assertClassNotFinal(RoutesCommand::class);
+    }
+
+    public function testShouldHaveAsCommandAttributeWithCommandName()
+    {
+        $commandClass = RoutesCommand::class;
+
+        $reflectionClass = new \ReflectionClass($commandClass);
+
+        $attributes = $reflectionClass->getAttributes(AsCommand::class);
+
+        $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.');
+
+        // Get the first attribute instance (assuming there is only one AsCommand attribute)
+        $asCommandAttribute = $attributes[0];
+
+        // Verify the 'name' parameter value
+        $attributeInstance = $asCommandAttribute->newInstance();
+        $this->assertEquals('enqueue:routes', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.');
+    }
+
+    public function testShouldHaveCommandAliases()
+    {
+        $command = new RoutesCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $this->assertEquals(['debug:enqueue:routes'], $command->getAliases());
+    }
+
+    public function testShouldHaveExpectedOptions()
+    {
+        $command = new RoutesCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $options = $command->getDefinition()->getOptions();
+        $this->assertCount(2, $options);
+
+        $this->assertArrayHasKey('show-route-options', $options);
+        $this->assertArrayHasKey('client', $options);
+    }
+
+    public function testShouldHaveExpectedAttributes()
+    {
+        $command = new RoutesCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $arguments = $command->getDefinition()->getArguments();
+        $this->assertCount(0, $arguments);
+    }
+
+    public function testShouldOutputEmptyRouteCollection()
+    {
+        $routeCollection = new RouteCollection([]);
+
+        $command = new RoutesCommand(new Container([
+            'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $tester->execute([]);
+
+        $expectedOutput = <<<'OUTPUT'
+Found 0 routes
+
+
+OUTPUT;
+
+        $this->assertCommandOutput($expectedOutput, $tester);
+    }
+
+    public function testShouldUseFooDriver()
+    {
+        $routeCollection = new RouteCollection([
+            new Route('fooTopic', Route::TOPIC, 'processor'),
+        ]);
+
+        $defaultDriverMock = $this->createMock(DriverInterface::class);
+        $defaultDriverMock
+            ->expects($this->never())
+            ->method('getConfig')
+        ;
+
+        $defaultDriverMock
+            ->expects($this->never())
+            ->method('getRouteCollection')
+        ;
+
+        $fooDriverMock = $this->createDriverStub(Config::create(), $routeCollection);
+
+        $command = new RoutesCommand(new Container([
+            'enqueue.client.default.driver' => $defaultDriverMock,
+            'enqueue.client.foo.driver' => $fooDriverMock,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            '--client' => 'foo',
+        ]);
+
+        $this->assertStringContainsString('Found 1 routes', $tester->getDisplay());
+    }
+
+    public function testThrowIfClientNotFound()
+    {
+        $defaultDriverMock = $this->createMock(DriverInterface::class);
+        $defaultDriverMock
+            ->expects($this->never())
+            ->method('getConfig')
+        ;
+
+        $defaultDriverMock
+            ->expects($this->never())
+            ->method('getRouteCollection')
+        ;
+
+        $command = new RoutesCommand(new Container([
+            'enqueue.client.default.driver' => $defaultDriverMock,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Client "foo" is not supported.');
+        $tester->execute([
+            '--client' => 'foo',
+        ]);
+    }
+
+    public function testShouldOutputTopicRouteInfo()
+    {
+        $routeCollection = new RouteCollection([
+            new Route('fooTopic', Route::TOPIC, 'processor'),
+            new Route('barTopic', Route::TOPIC, 'processor'),
+        ]);
+
+        $command = new RoutesCommand(new Container([
+            'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $tester->execute([]);
+
+        $expectedOutput = <<<'OUTPUT'
+Found 2 routes
+
++-------+----------+--------------------+-----------+----------+
+| Type  | Source   | Queue              | Processor | Options  |
++-------+----------+--------------------+-----------+----------+
+| topic | fooTopic | default (prefixed) | processor | (hidden) |
+| topic | barTopic | default (prefixed) | processor | (hidden) |
++-------+----------+--------------------+-----------+----------+
+
+OUTPUT;
+
+        $this->assertCommandOutput($expectedOutput, $tester);
+    }
+
+    public function testShouldOutputCommandRouteInfo()
+    {
+        $routeCollection = new RouteCollection([
+            new Route('fooCommand', Route::COMMAND, 'processor', ['foo' => 'fooVal', 'bar' => 'barVal']),
+            new Route('barCommand', Route::COMMAND, 'processor', ['foo' => 'fooVal', 'bar' => 'barVal']),
+        ]);
+
+        $command = new RoutesCommand(new Container([
+            'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $tester->execute([]);
+
+        $expectedOutput = <<<'OUTPUT'
+Found 2 routes
+
++---------+------------+--------------------+-----------+----------+
+| Type    | Source     | Queue              | Processor | Options  |
++---------+------------+--------------------+-----------+----------+
+| command | fooCommand | default (prefixed) | processor | (hidden) |
+| command | barCommand | default (prefixed) | processor | (hidden) |
++---------+------------+--------------------+-----------+----------+
+
+OUTPUT;
+
+        $this->assertCommandOutput($expectedOutput, $tester);
+    }
+
+    public function testShouldCorrectlyOutputPrefixedCustomQueue()
+    {
+        $routeCollection = new RouteCollection([
+            new Route('fooCommand', Route::COMMAND, 'processor', ['queue' => 'foo']),
+            new Route('barTopic', Route::TOPIC, 'processor', ['queue' => 'bar']),
+        ]);
+
+        $command = new RoutesCommand(new Container([
+            'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $exitCode = $tester->execute([]);
+        $this->assertSame(0, $exitCode);
+
+        $expectedOutput = <<<'OUTPUT'
+Found 2 routes
+
++---------+------------+----------------+-----------+----------+
+| Type    | Source     | Queue          | Processor | Options  |
++---------+------------+----------------+-----------+----------+
+| topic   | barTopic   | bar (prefixed) | processor | (hidden) |
+| command | fooCommand | foo (prefixed) | processor | (hidden) |
++---------+------------+----------------+-----------+----------+
+
+OUTPUT;
+
+        $this->assertCommandOutput($expectedOutput, $tester);
+    }
+
+    public function testShouldCorrectlyOutputNotPrefixedCustomQueue()
+    {
+        $routeCollection = new RouteCollection([
+            new Route('fooCommand', Route::COMMAND, 'processor', ['queue' => 'foo', 'prefix_queue' => false]),
+            new Route('barTopic', Route::TOPIC, 'processor', ['queue' => 'bar', 'prefix_queue' => false]),
+        ]);
+
+        $command = new RoutesCommand(new Container([
+            'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $tester->execute([]);
+
+        $expectedOutput = <<<'OUTPUT'
+Found 2 routes
+
++---------+------------+-------------+-----------+----------+
+| Type    | Source     | Queue       | Processor | Options  |
++---------+------------+-------------+-----------+----------+
+| topic   | barTopic   | bar (as is) | processor | (hidden) |
+| command | fooCommand | foo (as is) | processor | (hidden) |
++---------+------------+-------------+-----------+----------+
+
+OUTPUT;
+
+        $this->assertCommandOutput($expectedOutput, $tester);
+    }
+
+    public function testShouldCorrectlyOutputExternalRoute()
+    {
+        $routeCollection = new RouteCollection([
+            new Route('fooCommand', Route::COMMAND, 'processor', ['external' => true]),
+            new Route('barTopic', Route::TOPIC, 'processor', ['external' => true]),
+        ]);
+
+        $command = new RoutesCommand(new Container([
+            'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $tester->execute([]);
+
+        $expectedOutput = <<<OUTPUT
+Found 2 routes
+
++---------+------------+--------------------+----------------+----------+
+| Type    | Source     | Queue              | Processor      | Options  |
++---------+------------+--------------------+----------------+----------+
+| topic   | barTopic   | default (prefixed) | n\a (external) | (hidden) |
+| command | fooCommand | default (prefixed) | n\a (external) | (hidden) |
++---------+------------+--------------------+----------------+----------+
+
+OUTPUT;
+
+        $this->assertCommandOutput($expectedOutput, $tester);
+    }
+
+    public function testShouldOutputRouteOptions()
+    {
+        $routeCollection = new RouteCollection([
+            new Route('fooCommand', Route::COMMAND, 'processor', ['foo' => 'fooVal']),
+            new Route('barTopic', Route::TOPIC, 'processor', ['bar' => 'barVal']),
+        ]);
+
+        $command = new RoutesCommand(new Container([
+            'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $tester->execute(['--show-route-options' => true]);
+
+        $expectedOutput = <<<'OUTPUT'
+Found 2 routes
+
++---------+------------+--------------------+-----------+----------------------+
+| Type    | Source     | Queue              | Processor | Options              |
++---------+------------+--------------------+-----------+----------------------+
+| topic   | barTopic   | default (prefixed) | processor | array (              |
+|         |            |                    |           |   'bar' => 'barVal', |
+|         |            |                    |           | )                    |
+| command | fooCommand | default (prefixed) | processor | array (              |
+|         |            |                    |           |   'foo' => 'fooVal', |
+|         |            |                    |           | )                    |
++---------+------------+--------------------+-----------+----------------------+
+
+OUTPUT;
+
+        $this->assertCommandOutput($expectedOutput, $tester);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface
+     */
+    private function createDriverStub(Config $config, RouteCollection $routeCollection): DriverInterface
+    {
+        $driverMock = $this->createMock(DriverInterface::class);
+        $driverMock
+            ->expects($this->any())
+            ->method('getConfig')
+            ->willReturn($config)
+        ;
+
+        $driverMock
+            ->expects($this->any())
+            ->method('getRouteCollection')
+            ->willReturn($routeCollection)
+        ;
+
+        return $driverMock;
+    }
+
+    private function assertCommandOutput(string $expected, CommandTester $tester): void
+    {
+        $this->assertSame(0, $tester->getStatusCode());
+        $this->assertSame($expected, $tester->getDisplay());
+    }
+}
diff --git a/Tests/Symfony/Client/SetupBrokerCommandTest.php b/Tests/Symfony/Client/SetupBrokerCommandTest.php
index 740d6d1..c81c4e1 100644
--- a/Tests/Symfony/Client/SetupBrokerCommandTest.php
+++ b/Tests/Symfony/Client/SetupBrokerCommandTest.php
@@ -3,32 +3,74 @@
 namespace Enqueue\Tests\Symfony\Client;
 
 use Enqueue\Client\DriverInterface;
+use Enqueue\Container\Container;
 use Enqueue\Symfony\Client\SetupBrokerCommand;
+use Enqueue\Test\ClassExtensionTrait;
 use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Tester\CommandTester;
 
 class SetupBrokerCommandTest extends TestCase
 {
-    public function testCouldBeConstructedWithRequiredAttributes()
+    use ClassExtensionTrait;
+
+    public function testShouldBeSubClassOfCommand()
     {
-        new \Enqueue\Symfony\Client\SetupBrokerCommand($this->createClientDriverMock());
+        $this->assertClassExtends(Command::class, SetupBrokerCommand::class);
     }
 
-    public function testShouldHaveCommandName()
+    public function testShouldNotBeFinal()
     {
-        $command = new SetupBrokerCommand($this->createClientDriverMock());
+        $this->assertClassNotFinal(SetupBrokerCommand::class);
+    }
+
+    public function testShouldHaveAsCommandAttributeWithCommandName()
+    {
+        $commandClass = SetupBrokerCommand::class;
+
+        $reflectionClass = new \ReflectionClass($commandClass);
 
-        $this->assertEquals('enqueue:setup-broker', $command->getName());
+        $attributes = $reflectionClass->getAttributes(AsCommand::class);
+
+        $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.');
+
+        // Get the first attribute instance (assuming there is only one AsCommand attribute)
+        $asCommandAttribute = $attributes[0];
+
+        // Verify the 'name' parameter value
+        $attributeInstance = $asCommandAttribute->newInstance();
+        $this->assertEquals('enqueue:setup-broker', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.');
     }
 
     public function testShouldHaveCommandAliases()
     {
-        $command = new SetupBrokerCommand($this->createClientDriverMock());
+        $command = new SetupBrokerCommand($this->createMock(ContainerInterface::class), 'default');
 
         $this->assertEquals(['enq:sb'], $command->getAliases());
     }
 
-    public function testShouldCreateQueues()
+    public function testShouldHaveExpectedOptions()
+    {
+        $command = new SetupBrokerCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $options = $command->getDefinition()->getOptions();
+
+        $this->assertCount(1, $options);
+        $this->assertArrayHasKey('client', $options);
+    }
+
+    public function testShouldHaveExpectedAttributes()
+    {
+        $command = new SetupBrokerCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $arguments = $command->getDefinition()->getArguments();
+
+        $this->assertCount(0, $arguments);
+    }
+
+    public function testShouldCallDriverSetupBrokerMethod()
     {
         $driver = $this->createClientDriverMock();
         $driver
@@ -36,16 +78,66 @@ public function testShouldCreateQueues()
             ->method('setupBroker')
         ;
 
-        $command = new SetupBrokerCommand($driver);
+        $command = new SetupBrokerCommand(new Container([
+            'enqueue.client.default.driver' => $driver,
+        ]), 'default');
 
         $tester = new CommandTester($command);
         $tester->execute([]);
 
-        $this->assertContains('Setup Broker', $tester->getDisplay());
+        $this->assertStringContainsString('Broker set up', $tester->getDisplay());
+    }
+
+    public function testShouldCallRequestedClientDriverSetupBrokerMethod()
+    {
+        $defaultDriver = $this->createClientDriverMock();
+        $defaultDriver
+            ->expects($this->never())
+            ->method('setupBroker')
+        ;
+
+        $fooDriver = $this->createClientDriverMock();
+        $fooDriver
+            ->expects($this->once())
+            ->method('setupBroker')
+        ;
+
+        $command = new SetupBrokerCommand(new Container([
+            'enqueue.client.default.driver' => $defaultDriver,
+            'enqueue.client.foo.driver' => $fooDriver,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            '--client' => 'foo',
+        ]);
+
+        $this->assertStringContainsString('Broker set up', $tester->getDisplay());
+    }
+
+    public function testShouldThrowIfClientNotFound()
+    {
+        $defaultDriver = $this->createClientDriverMock();
+        $defaultDriver
+            ->expects($this->never())
+            ->method('setupBroker')
+        ;
+
+        $command = new SetupBrokerCommand(new Container([
+            'enqueue.client.default.driver' => $defaultDriver,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Client "foo" is not supported.');
+        $tester->execute([
+            '--client' => 'foo',
+        ]);
     }
 
     /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface
+     * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface
      */
     private function createClientDriverMock()
     {
diff --git a/Tests/Symfony/Client/SimpleConsumeCommandTest.php b/Tests/Symfony/Client/SimpleConsumeCommandTest.php
new file mode 100644
index 0000000..21c491e
--- /dev/null
+++ b/Tests/Symfony/Client/SimpleConsumeCommandTest.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\DelegateProcessor;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Consumption\ChainExtension;
+use Enqueue\Consumption\QueueConsumerInterface;
+use Enqueue\Null\NullQueue;
+use Enqueue\Symfony\Client\ConsumeCommand;
+use Enqueue\Symfony\Client\SimpleConsumeCommand;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Tester\CommandTester;
+
+class SimpleConsumeCommandTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeSubClassOfConsumeCommand()
+    {
+        $this->assertClassExtends(ConsumeCommand::class, SimpleConsumeCommand::class);
+    }
+
+    public function testShouldNotBeFinal()
+    {
+        $this->assertClassNotFinal(SimpleConsumeCommand::class);
+    }
+
+    public function testShouldHaveExpectedOptions()
+    {
+        $command = new SimpleConsumeCommand($this->createQueueConsumerMock(), $this->createDriverStub(), $this->createDelegateProcessorMock());
+
+        $options = $command->getDefinition()->getOptions();
+
+        $this->assertCount(9, $options);
+        $this->assertArrayHasKey('memory-limit', $options);
+        $this->assertArrayHasKey('message-limit', $options);
+        $this->assertArrayHasKey('time-limit', $options);
+        $this->assertArrayHasKey('receive-timeout', $options);
+        $this->assertArrayHasKey('niceness', $options);
+        $this->assertArrayHasKey('client', $options);
+        $this->assertArrayHasKey('logger', $options);
+        $this->assertArrayHasKey('skip', $options);
+        $this->assertArrayHasKey('setup-broker', $options);
+    }
+
+    public function testShouldHaveExpectedAttributes()
+    {
+        $command = new SimpleConsumeCommand($this->createQueueConsumerMock(), $this->createDriverStub(), $this->createDelegateProcessorMock());
+
+        $arguments = $command->getDefinition()->getArguments();
+
+        $this->assertCount(1, $arguments);
+        $this->assertArrayHasKey('client-queue-names', $arguments);
+    }
+
+    public function testShouldBindDefaultQueueOnly()
+    {
+        $queue = new NullQueue('');
+
+        $routeCollection = new RouteCollection([]);
+
+        $processor = $this->createDelegateProcessorMock();
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->once())
+            ->method('bind')
+            ->with($this->identicalTo($queue), $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->once())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $driver = $this->createDriverStub($routeCollection);
+        $driver
+            ->expects($this->once())
+            ->method('createQueue')
+            ->with('default', true)
+            ->willReturn($queue)
+        ;
+
+        $command = new SimpleConsumeCommand($consumer, $driver, $processor);
+
+        $tester = new CommandTester($command);
+        $tester->execute([]);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|DelegateProcessor
+     */
+    private function createDelegateProcessorMock()
+    {
+        return $this->createMock(DelegateProcessor::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface
+     */
+    private function createQueueConsumerMock()
+    {
+        return $this->createMock(QueueConsumerInterface::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface
+     */
+    private function createDriverStub(?RouteCollection $routeCollection = null): DriverInterface
+    {
+        $driverMock = $this->createMock(DriverInterface::class);
+        $driverMock
+            ->expects($this->any())
+            ->method('getRouteCollection')
+            ->willReturn($routeCollection ?? new RouteCollection([]))
+        ;
+
+        $driverMock
+            ->expects($this->any())
+            ->method('getConfig')
+            ->willReturn(Config::create('aPrefix', 'anApp'))
+        ;
+
+        return $driverMock;
+    }
+}
diff --git a/Tests/Symfony/Client/SimpleProduceCommandTest.php b/Tests/Symfony/Client/SimpleProduceCommandTest.php
new file mode 100644
index 0000000..3ff81bf
--- /dev/null
+++ b/Tests/Symfony/Client/SimpleProduceCommandTest.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client;
+
+use Enqueue\Client\ProducerInterface;
+use Enqueue\Symfony\Client\ProduceCommand;
+use Enqueue\Symfony\Client\SimpleProduceCommand;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Tester\CommandTester;
+
+class SimpleProduceCommandTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeSubClassOfProduceCommand()
+    {
+        $this->assertClassExtends(ProduceCommand::class, SimpleProduceCommand::class);
+    }
+
+    public function testShouldNotBeFinal()
+    {
+        $this->assertClassNotFinal(SimpleProduceCommand::class);
+    }
+
+    public function testShouldHaveExpectedOptions()
+    {
+        $command = new SimpleProduceCommand($this->createProducerMock());
+
+        $options = $command->getDefinition()->getOptions();
+        $this->assertCount(4, $options);
+        $this->assertArrayHasKey('client', $options);
+        $this->assertArrayHasKey('topic', $options);
+        $this->assertArrayHasKey('command', $options);
+        $this->assertArrayHasKey('header', $options);
+    }
+
+    public function testShouldHaveExpectedAttributes()
+    {
+        $command = new SimpleProduceCommand($this->createProducerMock());
+
+        $arguments = $command->getDefinition()->getArguments();
+        $this->assertCount(1, $arguments);
+
+        $this->assertArrayHasKey('message', $arguments);
+    }
+
+    public function testThrowIfNeitherTopicNorCommandOptionsAreSet()
+    {
+        $producerMock = $this->createProducerMock();
+        $producerMock
+            ->expects($this->never())
+            ->method('sendEvent')
+        ;
+        $producerMock
+            ->expects($this->never())
+            ->method('sendCommand')
+        ;
+
+        $command = new SimpleProduceCommand($producerMock);
+
+        $tester = new CommandTester($command);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Either topic or command option should be set, none is set.');
+        $tester->execute([
+            'message' => 'theMessage',
+        ]);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface
+     */
+    private function createProducerMock()
+    {
+        return $this->createMock(ProducerInterface::class);
+    }
+}
diff --git a/Tests/Symfony/Client/SimpleRoutesCommandTest.php b/Tests/Symfony/Client/SimpleRoutesCommandTest.php
new file mode 100644
index 0000000..20ee454
--- /dev/null
+++ b/Tests/Symfony/Client/SimpleRoutesCommandTest.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client;
+
+use Enqueue\Client\Config;
+use Enqueue\Client\DriverInterface;
+use Enqueue\Client\RouteCollection;
+use Enqueue\Symfony\Client\RoutesCommand;
+use Enqueue\Symfony\Client\SimpleRoutesCommand;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Tester\CommandTester;
+
+class SimpleRoutesCommandTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeSubClassOfRoutesCommand()
+    {
+        $this->assertClassExtends(RoutesCommand::class, SimpleRoutesCommand::class);
+    }
+
+    public function testShouldNotBeFinal()
+    {
+        $this->assertClassNotFinal(SimpleRoutesCommand::class);
+    }
+
+    public function testShouldHaveCommandAliases()
+    {
+        $command = new SimpleRoutesCommand($this->createDriverMock());
+
+        $this->assertEquals(['debug:enqueue:routes'], $command->getAliases());
+    }
+
+    public function testShouldHaveExpectedOptions()
+    {
+        $command = new SimpleRoutesCommand($this->createDriverMock());
+
+        $options = $command->getDefinition()->getOptions();
+        $this->assertCount(2, $options);
+
+        $this->assertArrayHasKey('show-route-options', $options);
+        $this->assertArrayHasKey('client', $options);
+    }
+
+    public function testShouldHaveExpectedAttributes()
+    {
+        $command = new SimpleRoutesCommand($this->createDriverMock());
+
+        $arguments = $command->getDefinition()->getArguments();
+        $this->assertCount(0, $arguments);
+    }
+
+    public function testShouldOutputEmptyRouteCollection()
+    {
+        $routeCollection = new RouteCollection([]);
+
+        $command = new SimpleRoutesCommand($this->createDriverStub(Config::create(), $routeCollection));
+
+        $tester = new CommandTester($command);
+
+        $tester->execute([]);
+
+        $expectedOutput = <<<'OUTPUT'
+Found 0 routes
+
+
+OUTPUT;
+
+        $this->assertCommandOutput($expectedOutput, $tester);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createDriverMock(): DriverInterface
+    {
+        return $this->createMock(DriverInterface::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createDriverStub(Config $config, RouteCollection $routeCollection): DriverInterface
+    {
+        $driverMock = $this->createDriverMock();
+        $driverMock
+            ->expects($this->any())
+            ->method('getConfig')
+            ->willReturn($config)
+        ;
+
+        $driverMock
+            ->expects($this->any())
+            ->method('getRouteCollection')
+            ->willReturn($routeCollection)
+        ;
+
+        return $driverMock;
+    }
+
+    private function assertCommandOutput(string $expected, CommandTester $tester): void
+    {
+        $this->assertSame(0, $tester->getStatusCode());
+        $this->assertSame($expected, $tester->getDisplay());
+    }
+}
diff --git a/Tests/Symfony/Client/SimpleSetupBrokerCommandTest.php b/Tests/Symfony/Client/SimpleSetupBrokerCommandTest.php
new file mode 100644
index 0000000..3702dbf
--- /dev/null
+++ b/Tests/Symfony/Client/SimpleSetupBrokerCommandTest.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Client;
+
+use Enqueue\Client\DriverInterface;
+use Enqueue\Symfony\Client\SetupBrokerCommand;
+use Enqueue\Symfony\Client\SimpleSetupBrokerCommand;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Tester\CommandTester;
+
+class SimpleSetupBrokerCommandTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeSubClassOfSetupBrokerCommand()
+    {
+        $this->assertClassExtends(SetupBrokerCommand::class, SimpleSetupBrokerCommand::class);
+    }
+
+    public function testShouldNotBeFinal()
+    {
+        $this->assertClassNotFinal(SimpleSetupBrokerCommand::class);
+    }
+
+    public function testShouldHaveCommandAliases()
+    {
+        $command = new SimpleSetupBrokerCommand($this->createClientDriverMock());
+
+        $this->assertEquals(['enq:sb'], $command->getAliases());
+    }
+
+    public function testShouldHaveExpectedOptions()
+    {
+        $command = new SimpleSetupBrokerCommand($this->createClientDriverMock());
+
+        $options = $command->getDefinition()->getOptions();
+
+        $this->assertCount(1, $options);
+        $this->assertArrayHasKey('client', $options);
+    }
+
+    public function testShouldHaveExpectedAttributes()
+    {
+        $command = new SimpleSetupBrokerCommand($this->createClientDriverMock());
+
+        $arguments = $command->getDefinition()->getArguments();
+
+        $this->assertCount(0, $arguments);
+    }
+
+    public function testShouldCallDriverSetupBrokerMethod()
+    {
+        $driver = $this->createClientDriverMock();
+        $driver
+            ->expects($this->once())
+            ->method('setupBroker')
+        ;
+
+        $command = new SimpleSetupBrokerCommand($driver);
+
+        $tester = new CommandTester($command);
+        $tester->execute([]);
+
+        $this->assertStringContainsString('Broker set up', $tester->getDisplay());
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface
+     */
+    private function createClientDriverMock()
+    {
+        return $this->createMock(DriverInterface::class);
+    }
+}
diff --git a/Tests/Symfony/Consumption/ConfigurableConsumeCommandTest.php b/Tests/Symfony/Consumption/ConfigurableConsumeCommandTest.php
new file mode 100644
index 0000000..251e264
--- /dev/null
+++ b/Tests/Symfony/Consumption/ConfigurableConsumeCommandTest.php
@@ -0,0 +1,306 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Consumption;
+
+use Enqueue\ArrayProcessorRegistry;
+use Enqueue\Consumption\ChainExtension;
+use Enqueue\Consumption\QueueConsumerInterface;
+use Enqueue\Consumption\QueueSubscriberInterface;
+use Enqueue\Container\Container;
+use Enqueue\Symfony\Consumption\ConfigurableConsumeCommand;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Processor;
+use Interop\Queue\Queue as InteropQueue;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Tester\CommandTester;
+
+class ConfigurableConsumeCommandTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeSubClassOfCommand()
+    {
+        $this->assertClassExtends(Command::class, ConfigurableConsumeCommand::class);
+    }
+
+    public function testShouldNotBeFinal()
+    {
+        $this->assertClassNotFinal(ConfigurableConsumeCommand::class);
+    }
+
+    public function testShouldHaveAsCommandAttributeWithCommandName()
+    {
+        $commandClass = ConfigurableConsumeCommand::class;
+
+        $reflectionClass = new \ReflectionClass($commandClass);
+
+        $attributes = $reflectionClass->getAttributes(AsCommand::class);
+
+        $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.');
+
+        // Get the first attribute instance (assuming there is only one AsCommand attribute)
+        $asCommandAttribute = $attributes[0];
+
+        // Verify the 'name' parameter value
+        $attributeInstance = $asCommandAttribute->newInstance();
+        $this->assertEquals('enqueue:transport:consume', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.');
+    }
+
+    public function testShouldHaveExpectedOptions()
+    {
+        $command = new ConfigurableConsumeCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $options = $command->getDefinition()->getOptions();
+
+        $this->assertCount(7, $options);
+        $this->assertArrayHasKey('memory-limit', $options);
+        $this->assertArrayHasKey('message-limit', $options);
+        $this->assertArrayHasKey('time-limit', $options);
+        $this->assertArrayHasKey('receive-timeout', $options);
+        $this->assertArrayHasKey('niceness', $options);
+        $this->assertArrayHasKey('transport', $options);
+        $this->assertArrayHasKey('logger', $options);
+    }
+
+    public function testShouldHaveExpectedAttributes()
+    {
+        $command = new ConfigurableConsumeCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $arguments = $command->getDefinition()->getArguments();
+
+        $this->assertCount(2, $arguments);
+        $this->assertArrayHasKey('processor', $arguments);
+        $this->assertArrayHasKey('queues', $arguments);
+    }
+
+    public function testThrowIfNeitherQueueOptionNorProcessorImplementsQueueSubscriberInterface()
+    {
+        $processor = $this->createProcessor();
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->never())
+            ->method('bind')
+        ;
+        $consumer
+            ->expects($this->never())
+            ->method('consume')
+        ;
+
+        $command = new ConfigurableConsumeCommand(new Container([
+            'enqueue.transport.default.queue_consumer' => $consumer,
+            'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['aProcessor' => $processor]),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The queue is not provided. The processor must implement "Enqueue\Consumption\QueueSubscriberInterface" interface and it must return not empty array of queues or a queue set using as a second argument.');
+        $tester->execute([
+            'processor' => 'aProcessor',
+        ]);
+    }
+
+    public function testShouldExecuteConsumptionWithExplicitlySetQueue()
+    {
+        $processor = $this->createProcessor();
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->once())
+            ->method('bind')
+            ->with('queue-name', $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->once())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $command = new ConfigurableConsumeCommand(new Container([
+            'enqueue.transport.default.queue_consumer' => $consumer,
+            'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            'processor' => 'processor-service',
+            'queues' => ['queue-name'],
+        ]);
+    }
+
+    public function testThrowIfTransportNotDefined()
+    {
+        $processor = $this->createProcessor();
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->never())
+            ->method('bind')
+        ;
+        $consumer
+            ->expects($this->never())
+            ->method('consume')
+        ;
+
+        $command = new ConfigurableConsumeCommand(new Container([
+            'enqueue.transport.default.queue_consumer' => $consumer,
+            'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Transport "not-defined" is not supported.');
+        $tester->execute([
+            'processor' => 'processor-service',
+            'queues' => ['queue-name'],
+            '--transport' => 'not-defined',
+        ]);
+    }
+
+    public function testShouldExecuteConsumptionWithSeveralCustomQueues()
+    {
+        $processor = $this->createProcessor();
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->at(0))
+            ->method('bind')
+            ->with('queue-name', $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->at(1))
+            ->method('bind')
+            ->with('another-queue-name', $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->at(2))
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $command = new ConfigurableConsumeCommand(new Container([
+            'enqueue.transport.default.queue_consumer' => $consumer,
+            'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            'processor' => 'processor-service',
+            'queues' => ['queue-name', 'another-queue-name'],
+        ]);
+    }
+
+    public function testShouldExecuteConsumptionWhenProcessorImplementsQueueSubscriberInterface()
+    {
+        $processor = new class implements Processor, QueueSubscriberInterface {
+            public function process(InteropMessage $message, Context $context): void
+            {
+            }
+
+            public static function getSubscribedQueues()
+            {
+                return ['fooSubscribedQueues', 'barSubscribedQueues'];
+            }
+        };
+
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->at(0))
+            ->method('bind')
+            ->with('fooSubscribedQueues', $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->at(1))
+            ->method('bind')
+            ->with('barSubscribedQueues', $this->identicalTo($processor))
+        ;
+        $consumer
+            ->expects($this->at(2))
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $command = new ConfigurableConsumeCommand(new Container([
+            'enqueue.transport.default.queue_consumer' => $consumer,
+            'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            'processor' => 'processor-service',
+        ]);
+    }
+
+    public function testShouldExecuteConsumptionWithCustomTransportExplicitlySetQueue()
+    {
+        $processor = $this->createProcessor();
+
+        $fooConsumer = $this->createQueueConsumerMock();
+        $fooConsumer
+            ->expects($this->never())
+            ->method('bind')
+        ;
+        $fooConsumer
+            ->expects($this->never())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $barConsumer = $this->createQueueConsumerMock();
+        $barConsumer
+            ->expects($this->once())
+            ->method('bind')
+            ->with('queue-name', $this->identicalTo($processor))
+        ;
+        $barConsumer
+            ->expects($this->once())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $command = new ConfigurableConsumeCommand(new Container([
+            'enqueue.transport.foo.queue_consumer' => $fooConsumer,
+            'enqueue.transport.foo.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]),
+            'enqueue.transport.bar.queue_consumer' => $barConsumer,
+            'enqueue.transport.bar.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]),
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([
+            'processor' => 'processor-service',
+            'queues' => ['queue-name'],
+            '--transport' => 'bar',
+        ]);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|InteropQueue
+     */
+    protected function createQueueMock()
+    {
+        return $this->createMock(InteropQueue::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|Processor
+     */
+    protected function createProcessor()
+    {
+        return $this->createMock(Processor::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface
+     */
+    protected function createQueueConsumerMock()
+    {
+        return $this->createMock(QueueConsumerInterface::class);
+    }
+}
diff --git a/Tests/Symfony/Consumption/ConsumeCommandTest.php b/Tests/Symfony/Consumption/ConsumeCommandTest.php
new file mode 100644
index 0000000..f07bef0
--- /dev/null
+++ b/Tests/Symfony/Consumption/ConsumeCommandTest.php
@@ -0,0 +1,247 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Consumption;
+
+use Enqueue\Consumption\ChainExtension;
+use Enqueue\Consumption\Context\Start;
+use Enqueue\Consumption\ExtensionInterface;
+use Enqueue\Consumption\QueueConsumer;
+use Enqueue\Consumption\QueueConsumerInterface;
+use Enqueue\Container\Container;
+use Enqueue\Null\NullQueue;
+use Enqueue\Symfony\Consumption\ConsumeCommand;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Consumer;
+use Interop\Queue\Context as InteropContext;
+use Interop\Queue\Exception\SubscriptionConsumerNotSupportedException;
+use Interop\Queue\Queue;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Tester\CommandTester;
+
+class ConsumeCommandTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeSubClassOfCommand()
+    {
+        $this->assertClassExtends(Command::class, ConsumeCommand::class);
+    }
+
+    public function testShouldNotBeFinal()
+    {
+        $this->assertClassNotFinal(ConsumeCommand::class);
+    }
+
+    public function testShouldHaveAsCommandAttributeWithCommandName()
+    {
+        $commandClass = ConsumeCommand::class;
+
+        $reflectionClass = new \ReflectionClass($commandClass);
+
+        $attributes = $reflectionClass->getAttributes(AsCommand::class);
+
+        $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.');
+
+        // Get the first attribute instance (assuming there is only one AsCommand attribute)
+        $asCommandAttribute = $attributes[0];
+
+        // Verify the 'name' parameter value
+        $attributeInstance = $asCommandAttribute->newInstance();
+        $this->assertEquals('enqueue:transport:consume', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.');
+    }
+
+    public function testShouldHaveExpectedOptions()
+    {
+        $command = new ConsumeCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $options = $command->getDefinition()->getOptions();
+
+        $this->assertCount(7, $options);
+        $this->assertArrayHasKey('memory-limit', $options);
+        $this->assertArrayHasKey('message-limit', $options);
+        $this->assertArrayHasKey('time-limit', $options);
+        $this->assertArrayHasKey('receive-timeout', $options);
+        $this->assertArrayHasKey('niceness', $options);
+        $this->assertArrayHasKey('transport', $options);
+        $this->assertArrayHasKey('logger', $options);
+    }
+
+    public function testShouldHaveExpectedAttributes()
+    {
+        $command = new ConsumeCommand($this->createMock(ContainerInterface::class), 'default');
+
+        $arguments = $command->getDefinition()->getArguments();
+
+        $this->assertCount(0, $arguments);
+    }
+
+    public function testShouldExecuteDefaultConsumption()
+    {
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->once())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.transport.default.queue_consumer' => $consumer,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute([]);
+    }
+
+    public function testShouldExecuteCustomConsumption()
+    {
+        $defaultConsumer = $this->createQueueConsumerMock();
+        $defaultConsumer
+            ->expects($this->never())
+            ->method('consume')
+        ;
+
+        $customConsumer = $this->createQueueConsumerMock();
+        $customConsumer
+            ->expects($this->once())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.transport.default.queue_consumer' => $defaultConsumer,
+            'enqueue.transport.custom.queue_consumer' => $customConsumer,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+        $tester->execute(['--transport' => 'custom']);
+    }
+
+    public function testThrowIfNotDefinedTransportRequested()
+    {
+        $defaultConsumer = $this->createQueueConsumerMock();
+        $defaultConsumer
+            ->expects($this->never())
+            ->method('consume')
+        ;
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.transport.default.queue_consumer' => $defaultConsumer,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Transport "not-defined" is not supported.');
+        $tester->execute(['--transport' => 'not-defined']);
+    }
+
+    public function testShouldReturnExitStatusIfSet()
+    {
+        $testExitCode = 678;
+
+        $stubExtension = $this->createExtension();
+
+        $stubExtension
+            ->expects($this->once())
+            ->method('onStart')
+            ->with($this->isInstanceOf(Start::class))
+            ->willReturnCallback(function (Start $context) use ($testExitCode) {
+                $context->interruptExecution($testExitCode);
+            })
+        ;
+
+        $consumer = new QueueConsumer($this->createContextStub(), $stubExtension);
+
+        $command = new ConsumeCommand(new Container([
+            'enqueue.transport.default.queue_consumer' => $consumer,
+        ]), 'default');
+
+        $tester = new CommandTester($command);
+
+        $tester->execute([]);
+
+        $this->assertEquals($testExitCode, $tester->getStatusCode());
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface
+     */
+    private function createQueueConsumerMock()
+    {
+        return $this->createMock(QueueConsumerInterface::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createContextWithoutSubscriptionConsumerMock(): InteropContext
+    {
+        $contextMock = $this->createMock(InteropContext::class);
+        $contextMock
+            ->expects($this->any())
+            ->method('createSubscriptionConsumer')
+            ->willThrowException(SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt())
+        ;
+
+        return $contextMock;
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|InteropContext
+     */
+    private function createContextStub(?Consumer $consumer = null): InteropContext
+    {
+        $context = $this->createContextWithoutSubscriptionConsumerMock();
+        $context
+            ->expects($this->any())
+            ->method('createQueue')
+            ->willReturnCallback(function (string $queueName) {
+                return new NullQueue($queueName);
+            })
+        ;
+        $context
+            ->expects($this->any())
+            ->method('createConsumer')
+            ->willReturnCallback(function (Queue $queue) use ($consumer) {
+                return $consumer ?: $this->createConsumerStub($queue);
+            })
+        ;
+
+        return $context;
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|ExtensionInterface
+     */
+    private function createExtension()
+    {
+        return $this->createMock(ExtensionInterface::class);
+    }
+
+    /**
+     * @param mixed|null $queue
+     *
+     * @return \PHPUnit\Framework\MockObject\MockObject|Consumer
+     */
+    private function createConsumerStub($queue = null): Consumer
+    {
+        if (null === $queue) {
+            $queue = 'queue';
+        }
+        if (is_string($queue)) {
+            $queue = new NullQueue($queue);
+        }
+
+        $consumerMock = $this->createMock(Consumer::class);
+        $consumerMock
+            ->expects($this->any())
+            ->method('getQueue')
+            ->willReturn($queue)
+        ;
+
+        return $consumerMock;
+    }
+}
diff --git a/Tests/Symfony/Consumption/ConsumeMessagesCommandTest.php b/Tests/Symfony/Consumption/ConsumeMessagesCommandTest.php
deleted file mode 100644
index d83505a..0000000
--- a/Tests/Symfony/Consumption/ConsumeMessagesCommandTest.php
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Symfony\Consumption;
-
-use Enqueue\Consumption\ChainExtension;
-use Enqueue\Consumption\QueueConsumer;
-use Enqueue\Symfony\Consumption\ConsumeMessagesCommand;
-use Interop\Queue\PsrContext;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Console\Tester\CommandTester;
-
-class ConsumeMessagesCommandTest extends TestCase
-{
-    public function testCouldBeConstructedWithRequiredAttributes()
-    {
-        new ConsumeMessagesCommand($this->createQueueConsumerMock());
-    }
-
-    public function testShouldHaveCommandName()
-    {
-        $command = new ConsumeMessagesCommand($this->createQueueConsumerMock());
-
-        $this->assertEquals('enqueue:transport:consume', $command->getName());
-    }
-
-    public function testShouldHaveExpectedOptions()
-    {
-        $command = new ConsumeMessagesCommand($this->createQueueConsumerMock());
-
-        $options = $command->getDefinition()->getOptions();
-
-        $this->assertCount(5, $options);
-        $this->assertArrayHasKey('memory-limit', $options);
-        $this->assertArrayHasKey('message-limit', $options);
-        $this->assertArrayHasKey('time-limit', $options);
-        $this->assertArrayHasKey('idle-timeout', $options);
-        $this->assertArrayHasKey('receive-timeout', $options);
-    }
-
-    public function testShouldHaveExpectedAttributes()
-    {
-        $command = new ConsumeMessagesCommand($this->createQueueConsumerMock());
-
-        $arguments = $command->getDefinition()->getArguments();
-
-        $this->assertCount(0, $arguments);
-    }
-
-    public function testShouldExecuteConsumption()
-    {
-        $context = $this->createContextMock();
-        $context
-            ->expects($this->never())
-            ->method('close')
-        ;
-
-        $consumer = $this->createQueueConsumerMock();
-        $consumer
-            ->expects($this->once())
-            ->method('consume')
-            ->with($this->isInstanceOf(ChainExtension::class))
-        ;
-
-        $command = new ConsumeMessagesCommand($consumer);
-
-        $tester = new CommandTester($command);
-        $tester->execute([]);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext
-     */
-    private function createContextMock()
-    {
-        return $this->createMock(PsrContext::class);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|QueueConsumer
-     */
-    private function createQueueConsumerMock()
-    {
-        return $this->createMock(QueueConsumer::class);
-    }
-}
diff --git a/Tests/Symfony/Consumption/ContainerAwareConsumeMessagesCommandTest.php b/Tests/Symfony/Consumption/ContainerAwareConsumeMessagesCommandTest.php
deleted file mode 100644
index b451dc7..0000000
--- a/Tests/Symfony/Consumption/ContainerAwareConsumeMessagesCommandTest.php
+++ /dev/null
@@ -1,207 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Symfony\Consumption;
-
-use Enqueue\Consumption\ChainExtension;
-use Enqueue\Consumption\QueueConsumer;
-use Enqueue\Symfony\Consumption\ContainerAwareConsumeMessagesCommand;
-use Enqueue\Tests\Symfony\Consumption\Mock\QueueSubscriberProcessor;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrProcessor;
-use Interop\Queue\PsrQueue;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Console\Tester\CommandTester;
-use Symfony\Component\DependencyInjection\Container;
-
-class ContainerAwareConsumeMessagesCommandTest extends TestCase
-{
-    public function testCouldBeConstructedWithRequiredAttributes()
-    {
-        new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock());
-    }
-
-    public function testShouldHaveCommandName()
-    {
-        $command = new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock());
-
-        $this->assertEquals('enqueue:transport:consume', $command->getName());
-    }
-
-    public function testShouldHaveExpectedOptions()
-    {
-        $command = new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock());
-
-        $options = $command->getDefinition()->getOptions();
-
-        $this->assertCount(6, $options);
-        $this->assertArrayHasKey('memory-limit', $options);
-        $this->assertArrayHasKey('message-limit', $options);
-        $this->assertArrayHasKey('time-limit', $options);
-        $this->assertArrayHasKey('queue', $options);
-        $this->assertArrayHasKey('idle-timeout', $options);
-        $this->assertArrayHasKey('receive-timeout', $options);
-    }
-
-    public function testShouldHaveExpectedAttributes()
-    {
-        $command = new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock());
-
-        $arguments = $command->getDefinition()->getArguments();
-
-        $this->assertCount(1, $arguments);
-        $this->assertArrayHasKey('processor-service', $arguments);
-    }
-
-    public function testShouldThrowExceptionIfProcessorInstanceHasWrongClass()
-    {
-        $container = new Container();
-        $container->set('processor-service', new \stdClass());
-
-        $command = new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock());
-        $command->setContainer($container);
-
-        $tester = new CommandTester($command);
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('Invalid message processor service given. It must be an instance of Interop\Queue\PsrProcessor but stdClass');
-        $tester->execute([
-            'processor-service' => 'processor-service',
-            '--queue' => ['queue-name'],
-        ]);
-    }
-
-    public function testThrowIfNeitherQueueOptionNorProcessorImplementsQueueSubscriberInterface()
-    {
-        $processor = $this->createProcessor();
-
-        $consumer = $this->createQueueConsumerMock();
-        $consumer
-            ->expects($this->never())
-            ->method('bind')
-        ;
-        $consumer
-            ->expects($this->never())
-            ->method('consume')
-        ;
-
-        $container = new Container();
-        $container->set('processor-service', $processor);
-
-        $command = new ContainerAwareConsumeMessagesCommand($consumer);
-        $command->setContainer($container);
-
-        $tester = new CommandTester($command);
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('The queues are not provided. The processor must implement "Enqueue\Consumption\QueueSubscriberInterface" interface and it must return not empty array of queues or queues set using --queue option.');
-        $tester->execute([
-            'processor-service' => 'processor-service',
-        ]);
-    }
-
-    public function testShouldExecuteConsumptionWithExplicitlySetQueueViaQueueOption()
-    {
-        $processor = $this->createProcessor();
-
-        $context = $this->createContextMock();
-        $context
-            ->expects($this->never())
-            ->method('close')
-        ;
-
-        $consumer = $this->createQueueConsumerMock();
-        $consumer
-            ->expects($this->once())
-            ->method('bind')
-            ->with('queue-name', $this->identicalTo($processor))
-        ;
-        $consumer
-            ->expects($this->once())
-            ->method('consume')
-            ->with($this->isInstanceOf(ChainExtension::class))
-        ;
-
-        $container = new Container();
-        $container->set('processor-service', $processor);
-
-        $command = new ContainerAwareConsumeMessagesCommand($consumer);
-        $command->setContainer($container);
-
-        $tester = new CommandTester($command);
-        $tester->execute([
-            'processor-service' => 'processor-service',
-            '--queue' => ['queue-name'],
-        ]);
-    }
-
-    public function testShouldExecuteConsumptionWhenProcessorImplementsQueueSubscriberInterface()
-    {
-        $processor = new QueueSubscriberProcessor();
-
-        $context = $this->createContextMock();
-        $context
-            ->expects($this->never())
-            ->method('close')
-        ;
-
-        $consumer = $this->createQueueConsumerMock();
-        $consumer
-            ->expects($this->at(0))
-            ->method('bind')
-            ->with('fooSubscribedQueues', $this->identicalTo($processor))
-        ;
-        $consumer
-            ->expects($this->at(1))
-            ->method('bind')
-            ->with('barSubscribedQueues', $this->identicalTo($processor))
-        ;
-        $consumer
-            ->expects($this->at(2))
-            ->method('consume')
-            ->with($this->isInstanceOf(ChainExtension::class))
-        ;
-
-        $container = new Container();
-        $container->set('processor-service', $processor);
-
-        $command = new ContainerAwareConsumeMessagesCommand($consumer);
-        $command->setContainer($container);
-
-        $tester = new CommandTester($command);
-        $tester->execute([
-            'processor-service' => 'processor-service',
-        ]);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext
-     */
-    protected function createContextMock()
-    {
-        return $this->createMock(PsrContext::class);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrQueue
-     */
-    protected function createQueueMock()
-    {
-        return $this->createMock(PsrQueue::class);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor
-     */
-    protected function createProcessor()
-    {
-        return $this->createMock(PsrProcessor::class);
-    }
-
-    /**
-     * @return \PHPUnit_Framework_MockObject_MockObject|QueueConsumer
-     */
-    protected function createQueueConsumerMock()
-    {
-        return $this->createMock(QueueConsumer::class);
-    }
-}
diff --git a/Tests/Symfony/Consumption/LimitsExtensionsCommandTraitTest.php b/Tests/Symfony/Consumption/LimitsExtensionsCommandTraitTest.php
index d9704d3..f47a321 100644
--- a/Tests/Symfony/Consumption/LimitsExtensionsCommandTraitTest.php
+++ b/Tests/Symfony/Consumption/LimitsExtensionsCommandTraitTest.php
@@ -5,6 +5,7 @@
 use Enqueue\Consumption\Extension\LimitConsumedMessagesExtension;
 use Enqueue\Consumption\Extension\LimitConsumerMemoryExtension;
 use Enqueue\Consumption\Extension\LimitConsumptionTimeExtension;
+use Enqueue\Consumption\Extension\NicenessExtension;
 use Enqueue\Tests\Symfony\Consumption\Mock\LimitsExtensionsCommand;
 use PHPUnit\Framework\TestCase;
 use Symfony\Component\Console\Tester\CommandTester;
@@ -17,10 +18,11 @@ public function testShouldAddExtensionsOptions()
 
         $options = $trait->getDefinition()->getOptions();
 
-        $this->assertCount(3, $options);
+        $this->assertCount(4, $options);
         $this->assertArrayHasKey('memory-limit', $options);
         $this->assertArrayHasKey('message-limit', $options);
         $this->assertArrayHasKey('time-limit', $options);
+        $this->assertArrayHasKey('niceness', $options);
     }
 
     public function testShouldAddMessageLimitExtension()
@@ -57,7 +59,8 @@ public function testShouldAddTimeLimitExtension()
 
     public function testShouldThrowExceptionIfTimeLimitExpressionIsNotValid()
     {
-        $this->setExpectedException(\Exception::class, 'Failed to parse time string (time is not valid) at position');
+        $this->expectException(\Exception::class);
+        $this->expectExceptionMessage('Failed to parse time string (time is not valid) at position');
 
         $command = new LimitsExtensionsCommand('name');
 
@@ -104,4 +107,36 @@ public function testShouldAddThreeLimitExtensions()
         $this->assertInstanceOf(LimitConsumptionTimeExtension::class, $result[1]);
         $this->assertInstanceOf(LimitConsumerMemoryExtension::class, $result[2]);
     }
+
+    /**
+     * @dataProvider provideNicenessValues
+     */
+    public function testShouldAddNicenessExtension($inputValue, bool $enabled)
+    {
+        $command = new LimitsExtensionsCommand('name');
+        $tester = new CommandTester($command);
+        $tester->execute([
+            '--niceness' => $inputValue,
+        ]);
+
+        $result = $command->getExtensions();
+
+        if ($enabled) {
+            $this->assertCount(1, $result);
+            $this->assertInstanceOf(NicenessExtension::class, $result[0]);
+        } else {
+            $this->assertEmpty($result);
+        }
+    }
+
+    public function provideNicenessValues(): \Generator
+    {
+        yield [1, true];
+        yield ['1', true];
+        yield [-1.0, true];
+        yield ['100', true];
+        yield ['', false];
+        yield ['0', false];
+        yield [0.0, false];
+    }
 }
diff --git a/Tests/Symfony/Consumption/Mock/LimitsExtensionsCommand.php b/Tests/Symfony/Consumption/Mock/LimitsExtensionsCommand.php
index 7b67223..05e0c56 100644
--- a/Tests/Symfony/Consumption/Mock/LimitsExtensionsCommand.php
+++ b/Tests/Symfony/Consumption/Mock/LimitsExtensionsCommand.php
@@ -25,8 +25,10 @@ protected function configure()
         $this->configureLimitsExtensions();
     }
 
-    protected function execute(InputInterface $input, OutputInterface $output)
+    protected function execute(InputInterface $input, OutputInterface $output): int
     {
         $this->extensions = $this->getLimitsExtensions($input, $output);
+
+        return 0;
     }
 }
diff --git a/Tests/Symfony/Consumption/Mock/QueueConsumerOptionsCommand.php b/Tests/Symfony/Consumption/Mock/QueueConsumerOptionsCommand.php
index 88a0d8c..147a3b9 100644
--- a/Tests/Symfony/Consumption/Mock/QueueConsumerOptionsCommand.php
+++ b/Tests/Symfony/Consumption/Mock/QueueConsumerOptionsCommand.php
@@ -2,7 +2,7 @@
 
 namespace Enqueue\Tests\Symfony\Consumption\Mock;
 
-use Enqueue\Consumption\QueueConsumer;
+use Enqueue\Consumption\QueueConsumerInterface;
 use Enqueue\Symfony\Consumption\QueueConsumerOptionsCommandTrait;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputInterface;
@@ -13,11 +13,11 @@ class QueueConsumerOptionsCommand extends Command
     use QueueConsumerOptionsCommandTrait;
 
     /**
-     * @var QueueConsumer
+     * @var QueueConsumerInterface
      */
     private $consumer;
 
-    public function __construct(QueueConsumer $consumer)
+    public function __construct(QueueConsumerInterface $consumer)
     {
         parent::__construct('queue-consumer-options');
 
@@ -31,8 +31,10 @@ protected function configure()
         $this->configureQueueConsumerOptions();
     }
 
-    protected function execute(InputInterface $input, OutputInterface $output)
+    protected function execute(InputInterface $input, OutputInterface $output): int
     {
         $this->setQueueConsumerOptions($this->consumer, $input);
+
+        return 0;
     }
 }
diff --git a/Tests/Symfony/Consumption/Mock/QueueSubscriberProcessor.php b/Tests/Symfony/Consumption/Mock/QueueSubscriberProcessor.php
index e71ad16..a210b0e 100644
--- a/Tests/Symfony/Consumption/Mock/QueueSubscriberProcessor.php
+++ b/Tests/Symfony/Consumption/Mock/QueueSubscriberProcessor.php
@@ -3,14 +3,15 @@
 namespace Enqueue\Tests\Symfony\Consumption\Mock;
 
 use Enqueue\Consumption\QueueSubscriberInterface;
-use Interop\Queue\PsrContext;
-use Interop\Queue\PsrMessage;
-use Interop\Queue\PsrProcessor;
+use Interop\Queue\Context;
+use Interop\Queue\Message as InteropMessage;
+use Interop\Queue\Processor;
 
-class QueueSubscriberProcessor implements PsrProcessor, QueueSubscriberInterface
+class QueueSubscriberProcessor implements Processor, QueueSubscriberInterface
 {
-    public function process(PsrMessage $message, PsrContext $context)
+    public function process(InteropMessage $message, Context $context)
     {
+        return self::ACK;
     }
 
     public static function getSubscribedQueues()
diff --git a/Tests/Symfony/Consumption/QueueConsumerOptionsCommandTraitTest.php b/Tests/Symfony/Consumption/QueueConsumerOptionsCommandTraitTest.php
index 57106aa..b44c89a 100644
--- a/Tests/Symfony/Consumption/QueueConsumerOptionsCommandTraitTest.php
+++ b/Tests/Symfony/Consumption/QueueConsumerOptionsCommandTraitTest.php
@@ -2,7 +2,7 @@
 
 namespace Enqueue\Tests\Symfony\Consumption;
 
-use Enqueue\Consumption\QueueConsumer;
+use Enqueue\Consumption\QueueConsumerInterface;
 use Enqueue\Tests\Symfony\Consumption\Mock\QueueConsumerOptionsCommand;
 use PHPUnit\Framework\TestCase;
 use Symfony\Component\Console\Tester\CommandTester;
@@ -15,19 +15,13 @@ public function testShouldAddExtensionsOptions()
 
         $options = $trait->getDefinition()->getOptions();
 
-        $this->assertCount(2, $options);
-        $this->assertArrayHasKey('idle-timeout', $options);
+        $this->assertCount(1, $options);
         $this->assertArrayHasKey('receive-timeout', $options);
     }
 
     public function testShouldSetQueueConsumerOptions()
     {
         $consumer = $this->createQueueConsumer();
-        $consumer
-            ->expects($this->once())
-            ->method('setIdleTimeout')
-            ->with($this->identicalTo(123))
-        ;
         $consumer
             ->expects($this->once())
             ->method('setReceiveTimeout')
@@ -38,16 +32,15 @@ public function testShouldSetQueueConsumerOptions()
 
         $tester = new CommandTester($trait);
         $tester->execute([
-            '--idle-timeout' => '123',
             '--receive-timeout' => '456',
         ]);
     }
 
     /**
-     * @return QueueConsumer|\PHPUnit_Framework_MockObject_MockObject|QueueConsumer
+     * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface
      */
     private function createQueueConsumer()
     {
-        return $this->createMock(QueueConsumer::class);
+        return $this->createMock(QueueConsumerInterface::class);
     }
 }
diff --git a/Tests/Symfony/Consumption/SimpleConsumeCommandTest.php b/Tests/Symfony/Consumption/SimpleConsumeCommandTest.php
new file mode 100644
index 0000000..eeb38bf
--- /dev/null
+++ b/Tests/Symfony/Consumption/SimpleConsumeCommandTest.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\Consumption;
+
+use Enqueue\Consumption\ChainExtension;
+use Enqueue\Consumption\QueueConsumerInterface;
+use Enqueue\Symfony\Consumption\ConsumeCommand;
+use Enqueue\Symfony\Consumption\SimpleConsumeCommand;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Tester\CommandTester;
+
+class SimpleConsumeCommandTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeSubClassOfConsumeCommand()
+    {
+        $this->assertClassExtends(ConsumeCommand::class, SimpleConsumeCommand::class);
+    }
+
+    public function testShouldNotBeFinal()
+    {
+        $this->assertClassNotFinal(SimpleConsumeCommand::class);
+    }
+
+    public function testShouldHaveExpectedOptions()
+    {
+        $command = new SimpleConsumeCommand($this->createQueueConsumerMock());
+
+        $options = $command->getDefinition()->getOptions();
+
+        $this->assertCount(7, $options);
+        $this->assertArrayHasKey('memory-limit', $options);
+        $this->assertArrayHasKey('message-limit', $options);
+        $this->assertArrayHasKey('time-limit', $options);
+        $this->assertArrayHasKey('receive-timeout', $options);
+        $this->assertArrayHasKey('niceness', $options);
+        $this->assertArrayHasKey('transport', $options);
+        $this->assertArrayHasKey('logger', $options);
+    }
+
+    public function testShouldHaveExpectedAttributes()
+    {
+        $command = new SimpleConsumeCommand($this->createQueueConsumerMock());
+
+        $arguments = $command->getDefinition()->getArguments();
+
+        $this->assertCount(0, $arguments);
+    }
+
+    public function testShouldExecuteDefaultConsumption()
+    {
+        $consumer = $this->createQueueConsumerMock();
+        $consumer
+            ->expects($this->once())
+            ->method('consume')
+            ->with($this->isInstanceOf(ChainExtension::class))
+        ;
+
+        $command = new SimpleConsumeCommand($consumer);
+
+        $tester = new CommandTester($command);
+        $tester->execute([]);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface
+     */
+    private function createQueueConsumerMock()
+    {
+        return $this->createMock(QueueConsumerInterface::class);
+    }
+}
diff --git a/Tests/Symfony/ContainerProcessorRegistryTest.php b/Tests/Symfony/ContainerProcessorRegistryTest.php
new file mode 100644
index 0000000..5504e8e
--- /dev/null
+++ b/Tests/Symfony/ContainerProcessorRegistryTest.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Enqueue\Tests\Symfony;
+
+use Enqueue\ProcessorRegistryInterface;
+use Enqueue\Symfony\ContainerProcessorRegistry;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\Processor;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+
+class ContainerProcessorRegistryTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementProcessorRegistryInterface()
+    {
+        $this->assertClassImplements(ProcessorRegistryInterface::class, ContainerProcessorRegistry::class);
+    }
+
+    public function testShouldBeFinal()
+    {
+        $this->assertClassFinal(ContainerProcessorRegistry::class);
+    }
+
+    public function testShouldAllowGetProcessor()
+    {
+        $processorMock = $this->createProcessorMock();
+
+        $containerMock = $this->createContainerMock();
+        $containerMock
+            ->expects($this->once())
+            ->method('has')
+            ->with('processor-name')
+            ->willReturn(true)
+        ;
+        $containerMock
+            ->expects($this->once())
+            ->method('get')
+            ->with('processor-name')
+            ->willReturn($processorMock)
+        ;
+
+        $registry = new ContainerProcessorRegistry($containerMock);
+        $this->assertSame($processorMock, $registry->get('processor-name'));
+    }
+
+    public function testThrowErrorIfServiceDoesNotImplementProcessorReturnType()
+    {
+        $containerMock = $this->createContainerMock();
+        $containerMock
+            ->expects($this->once())
+            ->method('has')
+            ->with('processor-name')
+            ->willReturn(true)
+        ;
+        $containerMock
+            ->expects($this->once())
+            ->method('get')
+            ->with('processor-name')
+            ->willReturn(new \stdClass())
+        ;
+
+        $registry = new ContainerProcessorRegistry($containerMock);
+
+        $this->expectException(\TypeError::class);
+        // Exception messages vary slightly between versions
+        $this->expectExceptionMessageMatches(
+            '/Enqueue\\\\Symfony\\\\ContainerProcessorRegistry::get\(\).+ Interop\\\\Queue\\\\Processor,.*stdClass returned/'
+        );
+
+        $registry->get('processor-name');
+    }
+
+    public function testShouldThrowExceptionIfProcessorIsNotSet()
+    {
+        $containerMock = $this->createContainerMock();
+        $containerMock
+            ->expects($this->once())
+            ->method('has')
+            ->with('processor-name')
+            ->willReturn(false)
+        ;
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Service locator does not have a processor with name "processor-name".');
+
+        $registry = new ContainerProcessorRegistry($containerMock);
+        $registry->get('processor-name');
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createProcessorMock(): Processor
+    {
+        return $this->createMock(Processor::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject
+     */
+    private function createContainerMock(): ContainerInterface
+    {
+        return $this->createMock(ContainerInterface::class);
+    }
+}
diff --git a/Tests/Symfony/DefaultTransportFactoryTest.php b/Tests/Symfony/DefaultTransportFactoryTest.php
deleted file mode 100644
index 1c7b0bd..0000000
--- a/Tests/Symfony/DefaultTransportFactoryTest.php
+++ /dev/null
@@ -1,275 +0,0 @@
-<?php
-
-namespace Enqueue\Tests\Symfony;
-
-use Enqueue\Symfony\DefaultTransportFactory;
-use Enqueue\Symfony\TransportFactoryInterface;
-use Enqueue\Test\ClassExtensionTrait;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Config\Definition\Builder\TreeBuilder;
-use Symfony\Component\Config\Definition\Processor;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
-
-class DefaultTransportFactoryTest extends TestCase
-{
-    use ClassExtensionTrait;
-
-    public function testShouldImplementTransportFactoryInterface()
-    {
-        $this->assertClassImplements(TransportFactoryInterface::class, DefaultTransportFactory::class);
-    }
-
-    public function testCouldBeConstructedWithDefaultName()
-    {
-        $transport = new DefaultTransportFactory();
-
-        $this->assertEquals('default', $transport->getName());
-    }
-
-    public function testCouldBeConstructedWithCustomName()
-    {
-        $transport = new DefaultTransportFactory('theCustomName');
-
-        $this->assertEquals('theCustomName', $transport->getName());
-    }
-
-    public function testShouldAllowAddConfigurationAsAliasAsString()
-    {
-        $transport = new DefaultTransportFactory();
-        $tb = new TreeBuilder();
-        $rootNode = $tb->root('foo');
-
-        $transport->addConfiguration($rootNode);
-        $processor = new Processor();
-        $config = $processor->process($tb->buildTree(), ['the_alias']);
-
-        $this->assertEquals(['alias' => 'the_alias'], $config);
-    }
-
-    public function testShouldAllowAddConfigurationAsAliasAsOption()
-    {
-        $transport = new DefaultTransportFactory();
-        $tb = new TreeBuilder();
-        $rootNode = $tb->root('foo');
-
-        $transport->addConfiguration($rootNode);
-        $processor = new Processor();
-        $config = $processor->process($tb->buildTree(), [['alias' => 'the_alias']]);
-
-        $this->assertEquals(['alias' => 'the_alias'], $config);
-    }
-
-    public function testShouldAllowAddConfigurationAsDsn()
-    {
-        $transport = new DefaultTransportFactory();
-        $tb = new TreeBuilder();
-        $rootNode = $tb->root('foo');
-
-        $transport->addConfiguration($rootNode);
-        $processor = new Processor();
-        $config = $processor->process($tb->buildTree(), ['dsn://']);
-
-        $this->assertEquals(['dsn' => 'dsn://'], $config);
-    }
-
-    public function testShouldSetNullTransportByDefault()
-    {
-        $transport = new DefaultTransportFactory();
-        $tb = new TreeBuilder();
-        $rootNode = $tb->root('foo');
-
-        $transport->addConfiguration($rootNode);
-        $processor = new Processor();
-
-        $config = $processor->process($tb->buildTree(), [null]);
-        $this->assertEquals(['dsn' => 'null:'], $config);
-
-        $config = $processor->process($tb->buildTree(), ['']);
-        $this->assertEquals(['dsn' => 'null:'], $config);
-    }
-
-    public function testThrowIfNeitherDsnNorAliasConfigured()
-    {
-        $transport = new DefaultTransportFactory();
-        $tb = new TreeBuilder();
-        $rootNode = $tb->root('foo');
-
-        $transport->addConfiguration($rootNode);
-        $processor = new Processor();
-
-        $this->expectException(\LogicException::class);
-        $this->expectExceptionMessage('Either dsn or alias option must be set');
-        $processor->process($tb->buildTree(), [[]]);
-    }
-
-    public function testShouldCreateConnectionFactoryFromAlias()
-    {
-        $container = new ContainerBuilder();
-
-        $transport = new DefaultTransportFactory();
-
-        $serviceId = $transport->createConnectionFactory($container, ['alias' => 'foo']);
-
-        $this->assertEquals('enqueue.transport.default.connection_factory', $serviceId);
-
-        $this->assertTrue($container->hasAlias('enqueue.transport.default.connection_factory'));
-        $this->assertEquals(
-            'enqueue.transport.foo.connection_factory',
-            (string) $container->getAlias('enqueue.transport.default.connection_factory')
-        );
-
-        $this->assertTrue($container->hasAlias('enqueue.transport.connection_factory'));
-        $this->assertEquals(
-            'enqueue.transport.default.connection_factory',
-            (string) $container->getAlias('enqueue.transport.connection_factory')
-        );
-    }
-
-    public function testShouldCreateContextFromAlias()
-    {
-        $container = new ContainerBuilder();
-
-        $transport = new DefaultTransportFactory();
-
-        $serviceId = $transport->createContext($container, ['alias' => 'the_alias']);
-
-        $this->assertEquals('enqueue.transport.default.context', $serviceId);
-
-        $this->assertTrue($container->hasAlias($serviceId));
-        $context = $container->getAlias($serviceId);
-        $this->assertEquals('enqueue.transport.the_alias.context', (string) $context);
-
-        $this->assertTrue($container->hasAlias('enqueue.transport.context'));
-        $context = $container->getAlias('enqueue.transport.context');
-        $this->assertEquals($serviceId, (string) $context);
-    }
-
-    public function testShouldCreateDriverFromAlias()
-    {
-        $container = new ContainerBuilder();
-
-        $transport = new DefaultTransportFactory();
-
-        $driverId = $transport->createDriver($container, ['alias' => 'the_alias']);
-
-        $this->assertEquals('enqueue.client.default.driver', $driverId);
-
-        $this->assertTrue($container->hasAlias($driverId));
-        $context = $container->getAlias($driverId);
-        $this->assertEquals('enqueue.client.the_alias.driver', (string) $context);
-
-        $this->assertTrue($container->hasAlias('enqueue.client.driver'));
-        $context = $container->getAlias('enqueue.client.driver');
-        $this->assertEquals($driverId, (string) $context);
-    }
-
-    /**
-     * @dataProvider provideDSNs
-     *
-     * @param mixed $dsn
-     * @param mixed $expectedName
-     */
-    public function testShouldCreateConnectionFactoryFromDSN($dsn, $expectedName)
-    {
-        $container = new ContainerBuilder();
-
-        $transport = new DefaultTransportFactory();
-
-        $serviceId = $transport->createConnectionFactory($container, ['dsn' => $dsn]);
-
-        $this->assertEquals('enqueue.transport.default.connection_factory', $serviceId);
-
-        $this->assertTrue($container->hasAlias('enqueue.transport.default.connection_factory'));
-        $this->assertEquals(
-            sprintf('enqueue.transport.%s.connection_factory', $expectedName),
-            (string) $container->getAlias('enqueue.transport.default.connection_factory')
-        );
-
-        $this->assertTrue($container->hasAlias('enqueue.transport.connection_factory'));
-        $this->assertEquals(
-            'enqueue.transport.default.connection_factory',
-            (string) $container->getAlias('enqueue.transport.connection_factory')
-        );
-    }
-
-    /**
-     * @dataProvider provideDSNs
-     *
-     * @param mixed $dsn
-     * @param mixed $expectedName
-     */
-    public function testShouldCreateContextFromDsn($dsn, $expectedName)
-    {
-        $container = new ContainerBuilder();
-
-        $transport = new DefaultTransportFactory();
-
-        $serviceId = $transport->createContext($container, ['dsn' => $dsn]);
-
-        $this->assertEquals('enqueue.transport.default.context', $serviceId);
-
-        $this->assertTrue($container->hasAlias($serviceId));
-        $context = $container->getAlias($serviceId);
-        $this->assertEquals(
-            sprintf('enqueue.transport.%s.context', $expectedName),
-            (string) $context
-        );
-
-        $this->assertTrue($container->hasAlias('enqueue.transport.context'));
-        $context = $container->getAlias('enqueue.transport.context');
-        $this->assertEquals($serviceId, (string) $context);
-    }
-
-    /**
-     * @dataProvider provideDSNs
-     *
-     * @param mixed $dsn
-     * @param mixed $expectedName
-     */
-    public function testShouldCreateDriverFromDsn($dsn, $expectedName)
-    {
-        $container = new ContainerBuilder();
-
-        $transport = new DefaultTransportFactory();
-
-        $driverId = $transport->createDriver($container, ['dsn' => $dsn]);
-
-        $this->assertEquals('enqueue.client.default.driver', $driverId);
-
-        $this->assertTrue($container->hasAlias($driverId));
-        $context = $container->getAlias($driverId);
-        $this->assertEquals(
-            sprintf('enqueue.client.%s.driver', $expectedName),
-            (string) $context
-        );
-
-        $this->assertTrue($container->hasAlias('enqueue.client.driver'));
-        $context = $container->getAlias('enqueue.client.driver');
-        $this->assertEquals($driverId, (string) $context);
-    }
-
-    public static function provideDSNs()
-    {
-        yield ['amqp+ext:', 'default_amqp_ext'];
-
-        yield ['amqp+lib:', 'default_amqp_lib'];
-
-        yield ['amqp+bunny:', 'default_amqp_bunny'];
-
-        yield ['null:', 'default_null'];
-
-        yield ['file:', 'default_fs'];
-
-        yield ['mysql:', 'default_dbal'];
-
-        yield ['pgsql:', 'default_dbal'];
-
-        yield ['gps:', 'default_gps'];
-
-        yield ['sqs:', 'default_sqs'];
-
-        yield ['redis:', 'default_redis'];
-
-        yield ['stomp:', 'default_stomp'];
-    }
-}
diff --git a/Tests/Symfony/DependencyInjection/BuildConsumptionExtensionsPassTest.php b/Tests/Symfony/DependencyInjection/BuildConsumptionExtensionsPassTest.php
new file mode 100644
index 0000000..bdccd33
--- /dev/null
+++ b/Tests/Symfony/DependencyInjection/BuildConsumptionExtensionsPassTest.php
@@ -0,0 +1,283 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\DependencyInjection;
+
+use Enqueue\Client\ExtensionInterface;
+use Enqueue\Symfony\DependencyInjection\BuildConsumptionExtensionsPass;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
+
+class BuildConsumptionExtensionsPassTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementCompilerPassInterface()
+    {
+        $this->assertClassImplements(CompilerPassInterface::class, BuildConsumptionExtensionsPass::class);
+    }
+
+    public function testShouldBeFinal()
+    {
+        $this->assertClassFinal(BuildConsumptionExtensionsPass::class);
+    }
+
+    public function testThrowIfEnqueueTransportsParameterNotSet()
+    {
+        $pass = new BuildConsumptionExtensionsPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The "enqueue.transports" parameter must be set.');
+        $pass->process(new ContainerBuilder());
+    }
+
+    public function testThrowsIfNoConsumptionExtensionsServiceFoundForConfiguredTransport()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo', 'bar']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+
+        $pass = new BuildConsumptionExtensionsPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Service "enqueue.transport.foo.consumption_extensions" not found');
+        $pass->process($container);
+    }
+
+    public function testShouldRegisterTransportExtension()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+        $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.transport.consumption_extension', ['transport' => 'foo'])
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.transport.consumption_extension', ['transport' => 'foo'])
+        ;
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+            new Reference('aBarExtension'),
+        ], $extensions->getArgument(0));
+    }
+
+    public function testShouldIgnoreOtherTransportExtensions()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+        $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.transport.consumption_extension', ['transport' => 'foo'])
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.transport.consumption_extension', ['transport' => 'anotherName'])
+        ;
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+        ], $extensions->getArgument(0));
+    }
+
+    public function testShouldAddExtensionIfTransportAll()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+        $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.transport.consumption_extension', ['transport' => 'all'])
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.transport.consumption_extension', ['transport' => 'anotherName'])
+        ;
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+        ], $extensions->getArgument(0));
+    }
+
+    public function testShouldTreatTagsWithoutTransportAsDefaultTransport()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+        $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.transport.consumption_extension')
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.transport.consumption_extension')
+        ;
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+            new Reference('aBarExtension'),
+        ], $extensions->getArgument(0));
+    }
+
+    public function testShouldOrderExtensionsByPriority()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+        $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.transport.consumption_extension', ['priority' => 6]);
+        $container->setDefinition('foo_extension', $extension);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.transport.consumption_extension', ['priority' => -5]);
+        $container->setDefinition('bar_extension', $extension);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.transport.consumption_extension', ['priority' => 2]);
+        $container->setDefinition('baz_extension', $extension);
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        $orderedExtensions = $extensions->getArgument(0);
+
+        $this->assertCount(3, $orderedExtensions);
+        $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[0]);
+        $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[1]);
+        $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[2]);
+    }
+
+    public function testShouldAssumePriorityZeroIfPriorityIsNotSet()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+
+        $extensions = new Definition();
+        $extensions->addArgument([]);
+        $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.transport.consumption_extension');
+        $container->setDefinition('foo_extension', $extension);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.transport.consumption_extension', ['priority' => 1]);
+        $container->setDefinition('bar_extension', $extension);
+
+        $extension = new Definition();
+        $extension->addTag('enqueue.transport.consumption_extension', ['priority' => -1]);
+        $container->setDefinition('baz_extension', $extension);
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        $orderedExtensions = $extensions->getArgument(0);
+
+        $this->assertCount(3, $orderedExtensions);
+        $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[0]);
+        $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[1]);
+        $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[2]);
+    }
+
+    public function testShouldMergeWithAddedPreviously()
+    {
+        $extensions = new Definition();
+        $extensions->addArgument([
+            'aBarExtension' => 'aBarServiceIdAddedPreviously',
+            'aOloloExtension' => 'aOloloServiceIdAddedPreviously',
+        ]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+        $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.transport.consumption_extension')
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.transport.consumption_extension')
+        ;
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($extensions->getArgument(0));
+        $this->assertCount(4, $extensions->getArgument(0));
+    }
+
+    public function testShouldRegisterProcessorWithMatchedNameToCorrespondingRegistries()
+    {
+        $fooExtensions = new Definition();
+        $fooExtensions->addArgument([]);
+
+        $barExtensions = new Definition();
+        $barExtensions->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo', 'bar']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+        $container->setDefinition('enqueue.transport.foo.consumption_extensions', $fooExtensions);
+        $container->setDefinition('enqueue.transport.bar.consumption_extensions', $barExtensions);
+
+        $container->register('aFooExtension', ExtensionInterface::class)
+            ->addTag('enqueue.transport.consumption_extension', ['transport' => 'foo'])
+        ;
+        $container->register('aBarExtension', ExtensionInterface::class)
+            ->addTag('enqueue.transport.consumption_extension', ['transport' => 'bar'])
+        ;
+
+        $pass = new BuildConsumptionExtensionsPass();
+        $pass->process($container);
+
+        self::assertIsArray($fooExtensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aFooExtension'),
+        ], $fooExtensions->getArgument(0));
+
+        self::assertIsArray($barExtensions->getArgument(0));
+        $this->assertEquals([
+            new Reference('aBarExtension'),
+        ], $barExtensions->getArgument(0));
+    }
+}
diff --git a/Tests/Symfony/DependencyInjection/BuildProcessorRegistryPassTest.php b/Tests/Symfony/DependencyInjection/BuildProcessorRegistryPassTest.php
new file mode 100644
index 0000000..134c216
--- /dev/null
+++ b/Tests/Symfony/DependencyInjection/BuildProcessorRegistryPassTest.php
@@ -0,0 +1,214 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\DependencyInjection;
+
+use Enqueue\ProcessorRegistryInterface;
+use Enqueue\Symfony\DependencyInjection\BuildProcessorRegistryPass;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
+
+class BuildProcessorRegistryPassTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementCompilerPassInterface()
+    {
+        $this->assertClassImplements(CompilerPassInterface::class, BuildProcessorRegistryPass::class);
+    }
+
+    public function testShouldBeFinal()
+    {
+        $this->assertClassFinal(BuildProcessorRegistryPass::class);
+    }
+
+    public function testThrowIfEnqueueTransportsParameterNotSet()
+    {
+        $pass = new BuildProcessorRegistryPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The "enqueue.transports" parameter must be set.');
+        $pass->process(new ContainerBuilder());
+    }
+
+    public function testThrowsIfNoRegistryServiceFoundForConfiguredTransport()
+    {
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo', 'bar']);
+        $container->setParameter('enqueue.default_transport', 'baz');
+
+        $pass = new BuildProcessorRegistryPass();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Service "enqueue.transport.foo.processor_registry" not found');
+        $pass->process($container);
+    }
+
+    public function testShouldRegisterProcessorWithMatchedName()
+    {
+        $registry = new Definition(ProcessorRegistryInterface::class);
+        $registry->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+        $container->setDefinition('enqueue.transport.foo.processor_registry', $registry);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.transport.processor', ['transport' => 'foo'])
+        ;
+        $container->register('aProcessor', 'aProcessorClass')
+            ->addTag('enqueue.transport.processor', ['transport' => 'bar'])
+        ;
+
+        $pass = new BuildProcessorRegistryPass();
+
+        $pass->process($container);
+
+        $this->assertInstanceOf(Reference::class, $registry->getArgument(0));
+
+        $this->assertLocatorServices($container, $registry->getArgument(0), [
+            'aFooProcessor' => 'aFooProcessor',
+        ]);
+    }
+
+    public function testShouldRegisterProcessorWithMatchedNameToCorrespondingRegistries()
+    {
+        $fooRegistry = new Definition(ProcessorRegistryInterface::class);
+        $fooRegistry->addArgument([]);
+
+        $barRegistry = new Definition(ProcessorRegistryInterface::class);
+        $barRegistry->addArgument([]);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo', 'bar']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+        $container->setDefinition('enqueue.transport.foo.processor_registry', $fooRegistry);
+        $container->setDefinition('enqueue.transport.bar.processor_registry', $barRegistry);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.transport.processor', ['transport' => 'foo'])
+        ;
+        $container->register('aBarProcessor', 'aProcessorClass')
+            ->addTag('enqueue.transport.processor', ['transport' => 'bar'])
+        ;
+
+        $pass = new BuildProcessorRegistryPass();
+
+        $pass->process($container);
+
+        $this->assertInstanceOf(Reference::class, $fooRegistry->getArgument(0));
+        $this->assertLocatorServices($container, $fooRegistry->getArgument(0), [
+            'aFooProcessor' => 'aFooProcessor',
+        ]);
+
+        $this->assertInstanceOf(Reference::class, $barRegistry->getArgument(0));
+        $this->assertLocatorServices($container, $barRegistry->getArgument(0), [
+            'aBarProcessor' => 'aBarProcessor',
+        ]);
+    }
+
+    public function testShouldRegisterProcessorWithoutNameToDefaultTransport()
+    {
+        $registry = new Definition(ProcessorRegistryInterface::class);
+        $registry->addArgument(null);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+        $container->setDefinition('enqueue.transport.foo.processor_registry', $registry);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.transport.processor', [])
+        ;
+        $container->register('aProcessor', 'aProcessorClass')
+            ->addTag('enqueue.transport.processor', ['transport' => 'bar'])
+        ;
+
+        $pass = new BuildProcessorRegistryPass();
+
+        $pass->process($container);
+
+        $this->assertInstanceOf(Reference::class, $registry->getArgument(0));
+
+        $this->assertLocatorServices($container, $registry->getArgument(0), [
+            'aFooProcessor' => 'aFooProcessor',
+        ]);
+    }
+
+    public function testShouldRegisterProcessorIfTransportNameEqualsAll()
+    {
+        $registry = new Definition(ProcessorRegistryInterface::class);
+        $registry->addArgument(null);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+        $container->setDefinition('enqueue.transport.foo.processor_registry', $registry);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.transport.processor', ['transport' => 'all'])
+        ;
+        $container->register('aProcessor', 'aProcessorClass')
+            ->addTag('enqueue.transport.processor', ['transport' => 'bar'])
+        ;
+
+        $pass = new BuildProcessorRegistryPass();
+
+        $pass->process($container);
+
+        $this->assertInstanceOf(Reference::class, $registry->getArgument(0));
+
+        $this->assertLocatorServices($container, $registry->getArgument(0), [
+            'aFooProcessor' => 'aFooProcessor',
+        ]);
+    }
+
+    public function testShouldRegisterWithCustomProcessorName()
+    {
+        $registry = new Definition(ProcessorRegistryInterface::class);
+        $registry->addArgument(null);
+
+        $container = new ContainerBuilder();
+        $container->setParameter('enqueue.transports', ['foo']);
+        $container->setParameter('enqueue.default_transport', 'foo');
+        $container->setDefinition('enqueue.transport.foo.processor_registry', $registry);
+        $container->register('aFooProcessor', 'aProcessorClass')
+            ->addTag('enqueue.transport.processor', ['processor' => 'customProcessorName'])
+        ;
+
+        $pass = new BuildProcessorRegistryPass();
+
+        $pass->process($container);
+
+        $this->assertInstanceOf(Reference::class, $registry->getArgument(0));
+
+        $this->assertLocatorServices($container, $registry->getArgument(0), [
+            'customProcessorName' => 'aFooProcessor',
+        ]);
+    }
+
+    private function assertLocatorServices(ContainerBuilder $container, $locatorId, array $locatorServices)
+    {
+        $this->assertInstanceOf(Reference::class, $locatorId);
+        $locatorId = (string) $locatorId;
+
+        $this->assertTrue($container->hasDefinition($locatorId));
+        $this->assertMatchesRegularExpression('/\.?service_locator\..*?\.enqueue\./', $locatorId);
+
+        $match = [];
+        if (false == preg_match('/(\.?service_locator\..*?)\.enqueue\./', $locatorId, $match)) {
+            $this->fail('preg_match should not failed');
+        }
+
+        $this->assertTrue($container->hasDefinition($match[1]));
+        $locator = $container->getDefinition($match[1]);
+
+        $this->assertContainsOnly(ServiceClosureArgument::class, $locator->getArgument(0));
+        $actualServices = array_map(function (ServiceClosureArgument $value) {
+            return (string) $value->getValues()[0];
+        }, $locator->getArgument(0));
+
+        $this->assertEquals($locatorServices, $actualServices);
+    }
+}
diff --git a/Tests/Symfony/DependencyInjection/TransportFactoryTest.php b/Tests/Symfony/DependencyInjection/TransportFactoryTest.php
new file mode 100644
index 0000000..9094074
--- /dev/null
+++ b/Tests/Symfony/DependencyInjection/TransportFactoryTest.php
@@ -0,0 +1,478 @@
+<?php
+
+namespace Enqueue\Tests\Symfony\DependencyInjection;
+
+use Enqueue\Consumption\ChainExtension;
+use Enqueue\Consumption\QueueConsumer;
+use Enqueue\Rpc\RpcClient;
+use Enqueue\Rpc\RpcFactory;
+use Enqueue\Symfony\DependencyInjection\TransportFactory;
+use Enqueue\Test\ClassExtensionTrait;
+use Interop\Queue\ConnectionFactory;
+use Interop\Queue\Context;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Config\Definition\Builder\NodeDefinition;
+use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
+use Symfony\Component\Config\Definition\Processor;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\Reference;
+
+class TransportFactoryTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldBeFinal()
+    {
+        $this->assertClassFinal(TransportFactory::class);
+    }
+
+    public function testThrowIfEmptyNameGivenOnConstruction()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The name could not be empty.');
+
+        new TransportFactory('');
+    }
+
+    public function testShouldAllowAddConfigurationAsStringDsn()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+
+        $processor = new Processor();
+        $config = $processor->process($tb->buildTree(), [['transport' => 'dsn://']]);
+
+        $this->assertEquals([
+            'transport' => [
+                'dsn' => 'dsn://',
+            ],
+        ], $config);
+    }
+
+    /**
+     * @see https://github.com/php-enqueue/enqueue-dev/issues/356
+     *
+     * @group bug
+     */
+    public function testShouldAllowAddConfigurationAsDsnWithoutSlashes()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+
+        $processor = new Processor();
+        $config = $processor->process($tb->buildTree(), [['transport' => 'dsn:']]);
+
+        $this->assertEquals([
+            'transport' => [
+                'dsn' => 'dsn:',
+            ],
+        ], $config);
+    }
+
+    public function testShouldSetNullTransportIfNullGiven()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+
+        $processor = new Processor();
+        $config = $processor->process($tb->buildTree(), [['transport' => null]]);
+
+        $this->assertEquals([
+            'transport' => [
+                'dsn' => 'null:',
+            ],
+        ], $config);
+    }
+
+    public function testShouldSetNullTransportIfEmptyStringGiven()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+
+        $processor = new Processor();
+        $config = $processor->process($tb->buildTree(), [['transport' => '']]);
+
+        $this->assertEquals([
+            'transport' => [
+                'dsn' => 'null:',
+            ],
+        ], $config);
+    }
+
+    public function testShouldSetNullTransportIfEmptyArrayGiven()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+
+        $processor = new Processor();
+        $config = $processor->process($tb->buildTree(), [['transport' => []]]);
+
+        $this->assertEquals([
+            'transport' => [
+                'dsn' => 'null:',
+            ],
+        ], $config);
+    }
+
+    public function testThrowIfEmptyDsnGiven()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+
+        $processor = new Processor();
+        $this->expectException(InvalidConfigurationException::class);
+        $this->expectExceptionMessage('The path "foo.transport.dsn" cannot contain an empty value, but got "".');
+        $processor->process($tb->buildTree(), [['transport' => ['dsn' => '']]]);
+    }
+
+    public function testThrowIfFactoryClassAndFactoryServiceSetAtTheSameTime()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+
+        $processor = new Processor();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('Both options factory_class and factory_service are set. Please choose one.');
+        $processor->process($tb->buildTree(), [[
+            'transport' => [
+                'dsn' => 'foo:',
+                'factory_class' => 'aFactoryClass',
+                'factory_service' => 'aFactoryService',
+            ], ]]);
+    }
+
+    public function testThrowIfConnectionFactoryClassUsedWithFactoryClassAtTheSameTime()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+
+        $processor = new Processor();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The option connection_factory_class must not be used with factory_class or factory_service at the same time. Please choose one.');
+        $processor->process($tb->buildTree(), [[
+            'transport' => [
+                'dsn' => 'foo:',
+                'connection_factory_class' => 'aFactoryClass',
+                'factory_service' => 'aFactoryService',
+            ], ]]);
+    }
+
+    public function testThrowIfConnectionFactoryClassUsedWithFactoryServiceAtTheSameTime()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+        $processor = new Processor();
+
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionMessage('The option connection_factory_class must not be used with factory_class or factory_service at the same time. Please choose one.');
+        $processor->process($tb->buildTree(), [[
+            'transport' => [
+                'dsn' => 'foo:',
+                'connection_factory_class' => 'aFactoryClass',
+                'factory_service' => 'aFactoryService',
+            ], ]]);
+    }
+
+    public function testShouldAllowSetFactoryClass()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+        $processor = new Processor();
+
+        $config = $processor->process($tb->buildTree(), [[
+            'transport' => [
+                'dsn' => 'foo:',
+                'factory_class' => 'theFactoryClass',
+            ], ]]);
+
+        $this->assertArrayHasKey('factory_class', $config['transport']);
+        $this->assertSame('theFactoryClass', $config['transport']['factory_class']);
+    }
+
+    public function testShouldAllowSetFactoryService()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+        $processor = new Processor();
+
+        $config = $processor->process($tb->buildTree(), [[
+            'transport' => [
+                'dsn' => 'foo:',
+                'factory_service' => 'theFactoryService',
+            ], ]]);
+
+        $this->assertArrayHasKey('factory_service', $config['transport']);
+        $this->assertSame('theFactoryService', $config['transport']['factory_service']);
+    }
+
+    public function testShouldAllowSetConnectionFactoryClass()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+        $processor = new Processor();
+
+        $config = $processor->process($tb->buildTree(), [[
+            'transport' => [
+                'dsn' => 'foo:',
+                'connection_factory_class' => 'theFactoryClass',
+            ], ]]);
+
+        $this->assertArrayHasKey('connection_factory_class', $config['transport']);
+        $this->assertSame('theFactoryClass', $config['transport']['connection_factory_class']);
+    }
+
+    public function testThrowIfExtraOptionGiven()
+    {
+        list($tb, $rootNode) = $this->getRootNode();
+
+        $rootNode->append(TransportFactory::getConfiguration());
+        $processor = new Processor();
+
+        $config = $processor->process($tb->buildTree(), [['transport' => ['dsn' => 'foo:', 'extraOption' => 'aVal']]]);
+        $this->assertEquals([
+            'transport' => [
+                'dsn' => 'foo:',
+                'extraOption' => 'aVal',
+            ], ], $config
+        );
+    }
+
+    public function testShouldBuildConnectionFactoryFromDSN()
+    {
+        $container = new ContainerBuilder();
+
+        $transport = new TransportFactory('default');
+
+        $config = [
+            'dsn' => 'foo://bar/baz',
+            'connection_factory_class' => null,
+            'factory_service' => null,
+            'factory_class' => null,
+        ];
+
+        $transport->buildConnectionFactory($container, $config);
+
+        $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory'));
+
+        $this->assertNotEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory());
+        $this->assertEquals(
+            [new Reference('enqueue.transport.default.connection_factory_factory'), 'create'],
+            $container->getDefinition('enqueue.transport.default.connection_factory')->getFactory())
+        ;
+        $this->assertSame(
+            [['dsn' => 'foo://bar/baz']],
+            $container->getDefinition('enqueue.transport.default.connection_factory')->getArguments())
+        ;
+    }
+
+    public function testShouldBuildConnectionFactoryUsingCustomFactoryClass()
+    {
+        $container = new ContainerBuilder();
+
+        $transport = new TransportFactory('default');
+
+        $transport->buildConnectionFactory($container, ['dsn' => 'foo:', 'factory_class' => 'theFactoryClass']);
+
+        $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory_factory'));
+        $this->assertSame(
+            'theFactoryClass',
+            $container->getDefinition('enqueue.transport.default.connection_factory_factory')->getClass()
+        );
+
+        $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory'));
+
+        $this->assertNotEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory());
+        $this->assertEquals(
+            [new Reference('enqueue.transport.default.connection_factory_factory'), 'create'],
+            $container->getDefinition('enqueue.transport.default.connection_factory')->getFactory())
+        ;
+        $this->assertSame(
+            [['dsn' => 'foo:']],
+            $container->getDefinition('enqueue.transport.default.connection_factory')->getArguments())
+        ;
+    }
+
+    public function testShouldBuildConnectionFactoryUsingCustomFactoryService()
+    {
+        $container = new ContainerBuilder();
+
+        $transport = new TransportFactory('default');
+
+        $transport->buildConnectionFactory($container, ['dsn' => 'foo:', 'factory_service' => 'theFactoryService']);
+
+        $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory'));
+
+        $this->assertNotEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory());
+        $this->assertEquals(
+            [new Reference('theFactoryService'), 'create'],
+            $container->getDefinition('enqueue.transport.default.connection_factory')->getFactory())
+        ;
+        $this->assertSame(
+            [['dsn' => 'foo:']],
+            $container->getDefinition('enqueue.transport.default.connection_factory')->getArguments())
+        ;
+    }
+
+    public function testShouldBuildConnectionFactoryUsingConnectionFactoryClassWithoutFactory()
+    {
+        $container = new ContainerBuilder();
+
+        $transport = new TransportFactory('default');
+
+        $transport->buildConnectionFactory($container, ['dsn' => 'foo:', 'connection_factory_class' => 'theFactoryClass']);
+
+        $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory'));
+
+        $this->assertEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory());
+        $this->assertSame('theFactoryClass', $container->getDefinition('enqueue.transport.default.connection_factory')->getClass());
+        $this->assertSame(
+            [['dsn' => 'foo:']],
+            $container->getDefinition('enqueue.transport.default.connection_factory')->getArguments())
+        ;
+    }
+
+    public function testShouldBuildContext()
+    {
+        $container = new ContainerBuilder();
+        $container->register('enqueue.transport.default.connection_factory', ConnectionFactory::class);
+
+        $transport = new TransportFactory('default');
+
+        $transport->buildContext($container, []);
+
+        $this->assertNotEmpty($container->getDefinition('enqueue.transport.default.context')->getFactory());
+        $this->assertEquals(
+            [new Reference('enqueue.transport.default.connection_factory'), 'createContext'],
+            $container->getDefinition('enqueue.transport.default.context')->getFactory())
+        ;
+        $this->assertSame(
+            [],
+            $container->getDefinition('enqueue.transport.default.context')->getArguments())
+        ;
+    }
+
+    public function testThrowIfBuildContextCalledButConnectionFactoryServiceDoesNotExist()
+    {
+        $container = new ContainerBuilder();
+
+        $transport = new TransportFactory('default');
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The service "enqueue.transport.default.connection_factory" does not exist.');
+        $transport->buildContext($container, []);
+    }
+
+    public function testShouldBuildQueueConsumerWithDefaultOptions()
+    {
+        $container = new ContainerBuilder();
+        $container->register('enqueue.transport.default.context', Context::class);
+
+        $transport = new TransportFactory('default');
+
+        $transport->buildQueueConsumer($container, []);
+
+        $this->assertSame(10000, $container->getParameter('enqueue.transport.default.receive_timeout'));
+
+        $this->assertTrue($container->hasDefinition('enqueue.transport.default.consumption_extensions'));
+        $this->assertSame(ChainExtension::class, $container->getDefinition('enqueue.transport.default.consumption_extensions')->getClass());
+        $this->assertSame([[]], $container->getDefinition('enqueue.transport.default.consumption_extensions')->getArguments());
+
+        $this->assertTrue($container->hasDefinition('enqueue.transport.default.queue_consumer'));
+        $this->assertSame(QueueConsumer::class, $container->getDefinition('enqueue.transport.default.queue_consumer')->getClass());
+        $this->assertEquals([
+            new Reference('enqueue.transport.default.context'),
+            new Reference('enqueue.transport.default.consumption_extensions'),
+            [],
+            new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE),
+            '%enqueue.transport.default.receive_timeout%',
+        ], $container->getDefinition('enqueue.transport.default.queue_consumer')->getArguments());
+    }
+
+    public function testShouldBuildQueueConsumerWithCustomOptions()
+    {
+        $container = new ContainerBuilder();
+        $container->register('enqueue.transport.default.context', Context::class);
+
+        $transport = new TransportFactory('default');
+
+        $transport->buildQueueConsumer($container, [
+            'receive_timeout' => 567,
+        ]);
+
+        $this->assertSame(567, $container->getParameter('enqueue.transport.default.receive_timeout'));
+    }
+
+    public function testThrowIfBuildQueueConsumerCalledButContextServiceDoesNotExist()
+    {
+        $container = new ContainerBuilder();
+
+        $transport = new TransportFactory('default');
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The service "enqueue.transport.default.context" does not exist.');
+        $transport->buildQueueConsumer($container, []);
+    }
+
+    public function testShouldBuildRpcClientWithDefaultOptions()
+    {
+        $container = new ContainerBuilder();
+        $container->register('enqueue.transport.default.context', Context::class);
+
+        $transport = new TransportFactory('default');
+
+        $transport->buildRpcClient($container, []);
+
+        $this->assertTrue($container->hasDefinition('enqueue.transport.default.rpc_factory'));
+        $this->assertSame(RpcFactory::class, $container->getDefinition('enqueue.transport.default.rpc_factory')->getClass());
+
+        $this->assertTrue($container->hasDefinition('enqueue.transport.default.rpc_client'));
+        $this->assertSame(RpcClient::class, $container->getDefinition('enqueue.transport.default.rpc_client')->getClass());
+        $this->assertEquals([
+            new Reference('enqueue.transport.default.context'),
+            new Reference('enqueue.transport.default.rpc_factory'),
+        ], $container->getDefinition('enqueue.transport.default.rpc_client')->getArguments());
+    }
+
+    public function testThrowIfBuildRpcClientCalledButContextServiceDoesNotExist()
+    {
+        $container = new ContainerBuilder();
+
+        $transport = new TransportFactory('default');
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The service "enqueue.transport.default.context" does not exist.');
+        $transport->buildRpcClient($container, []);
+    }
+
+    /**
+     * @return [TreeBuilder, NodeDefinition]
+     */
+    private function getRootNode(): array
+    {
+        if (method_exists(TreeBuilder::class, 'getRootNode')) {
+            $tb = new TreeBuilder('foo');
+
+            return [$tb, $tb->getRootNode()];
+        }
+
+        $tb = new TreeBuilder();
+
+        return [$tb, $tb->root('foo')];
+    }
+}
diff --git a/Tests/Symfony/LazyProducerTest.php b/Tests/Symfony/LazyProducerTest.php
new file mode 100644
index 0000000..c8ba596
--- /dev/null
+++ b/Tests/Symfony/LazyProducerTest.php
@@ -0,0 +1,128 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Enqueue\Tests\Symfony;
+
+use Enqueue\Client\ProducerInterface;
+use Enqueue\Rpc\Promise;
+use Enqueue\Symfony\Client\LazyProducer;
+use Enqueue\Test\ClassExtensionTrait;
+use PHPUnit\Framework\TestCase;
+use Psr\Container\ContainerInterface;
+
+class LazyProducerTest extends TestCase
+{
+    use ClassExtensionTrait;
+
+    public function testShouldImplementProducerInterface()
+    {
+        $this->assertClassImplements(ProducerInterface::class, LazyProducer::class);
+    }
+
+    public function testShouldNotCallRealProducerInConstructor()
+    {
+        $containerMock = $this->createContainerMock();
+        $containerMock
+            ->expects($this->never())
+            ->method('get')
+        ;
+
+        new LazyProducer($containerMock, 'realProducerId');
+    }
+
+    public function testShouldProxyAllArgumentOnSendEvent()
+    {
+        $topic = 'theTopic';
+        $message = 'theMessage';
+
+        $realProducerMock = $this->createProducerMock();
+        $realProducerMock
+            ->expects($this->once())
+            ->method('sendEvent')
+            ->with($topic, $message)
+        ;
+
+        $containerMock = $this->createContainerMock();
+        $containerMock
+            ->expects($this->once())
+            ->method('get')
+            ->with('realProducerId')
+            ->willReturn($realProducerMock)
+        ;
+
+        $lazyProducer = new LazyProducer($containerMock, 'realProducerId');
+
+        $lazyProducer->sendEvent($topic, $message);
+    }
+
+    public function testShouldProxyAllArgumentOnSendCommand()
+    {
+        $command = 'theCommand';
+        $message = 'theMessage';
+        $needReply = false;
+
+        $realProducerMock = $this->createProducerMock();
+        $realProducerMock
+            ->expects($this->once())
+            ->method('sendCommand')
+            ->with($command, $message, $needReply)
+        ;
+
+        $containerMock = $this->createContainerMock();
+        $containerMock
+            ->expects($this->once())
+            ->method('get')
+            ->with('realProducerId')
+            ->willReturn($realProducerMock)
+        ;
+
+        $lazyProducer = new LazyProducer($containerMock, 'realProducerId');
+
+        $result = $lazyProducer->sendCommand($command, $message, $needReply);
+
+        $this->assertNull($result);
+    }
+
+    public function testShouldProxyReturnedPromiseBackOnSendCommand()
+    {
+        $expectedPromise = $this->createMock(Promise::class);
+
+        $realProducerMock = $this->createProducerMock();
+        $realProducerMock
+            ->expects($this->once())
+            ->method('sendCommand')
+            ->willReturn($expectedPromise)
+        ;
+
+        $containerMock = $this->createContainerMock();
+        $containerMock
+            ->expects($this->once())
+            ->method('get')
+            ->with('realProducerId')
+            ->willReturn($realProducerMock)
+        ;
+
+        $lazyProducer = new LazyProducer($containerMock, 'realProducerId');
+
+        $actualPromise = $lazyProducer->sendCommand('aCommand', 'aMessage', true);
+
+        $this->assertSame($expectedPromise, $actualPromise);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface
+     */
+    private function createProducerMock(): ProducerInterface
+    {
+        return $this->createMock(ProducerInterface::class);
+    }
+
+    /**
+     * @return \PHPUnit\Framework\MockObject\MockObject|ContainerInterface
+     */
+    private function createContainerMock(): ContainerInterface
+    {
+        return $this->createMock(ContainerInterface::class);
+    }
+}
diff --git a/Tests/Util/Fixtures/JsonSerializableClass.php b/Tests/Util/Fixtures/JsonSerializableClass.php
index b612978..1a77ce0 100644
--- a/Tests/Util/Fixtures/JsonSerializableClass.php
+++ b/Tests/Util/Fixtures/JsonSerializableClass.php
@@ -6,7 +6,8 @@ class JsonSerializableClass implements \JsonSerializable
 {
     public $keyPublic = 'public';
 
-    public function jsonSerialize()
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize(): array
     {
         return [
             'key' => 'value',
diff --git a/Tests/Util/JSONTest.php b/Tests/Util/JSONTest.php
index c37862e..1a3df42 100644
--- a/Tests/Util/JSONTest.php
+++ b/Tests/Util/JSONTest.php
@@ -16,7 +16,8 @@ public function testShouldDecodeString()
 
     public function testThrowIfMalformedJson()
     {
-        $this->setExpectedException(\InvalidArgumentException::class, 'The malformed json given. ');
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The malformed json given. ');
         $this->assertSame(['foo' => 'fooVal'], JSON::decode('{]'));
     }
 
@@ -38,15 +39,11 @@ public function nonStringDataProvider()
 
     /**
      * @dataProvider nonStringDataProvider
-     *
-     * @param mixed $value
      */
     public function testShouldThrowExceptionIfInputIsNotString($value)
     {
-        $this->setExpectedException(
-            \InvalidArgumentException::class,
-            'Accept only string argument but got:'
-        );
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Accept only string argument but got:');
 
         $this->assertSame(0, JSON::decode($value));
     }
@@ -96,10 +93,8 @@ public function testShouldEncodeObjectOfJsonSerializableClass()
 
     public function testThrowIfValueIsResource()
     {
-        $this->setExpectedException(
-            \InvalidArgumentException::class,
-            'Could not encode value into json. Error 8 and message Type is not supported'
-        );
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Could not encode value into json. Error 8 and message Type is not supported');
 
         $resource = fopen('php://memory', 'r');
         fclose($resource);
diff --git a/Tests/Util/UUIDTest.php b/Tests/Util/UUIDTest.php
index ac30903..f21693e 100644
--- a/Tests/Util/UUIDTest.php
+++ b/Tests/Util/UUIDTest.php
@@ -11,7 +11,7 @@ public function testShouldGenerateUniqueId()
     {
         $uuid = UUID::generate();
 
-        $this->assertInternalType('string', $uuid);
+        $this->assertIsString($uuid);
         $this->assertEquals(36, strlen($uuid));
     }
 
diff --git a/Tests/Util/VarExportTest.php b/Tests/Util/VarExportTest.php
index 1d2384a..b71e78a 100644
--- a/Tests/Util/VarExportTest.php
+++ b/Tests/Util/VarExportTest.php
@@ -7,16 +7,8 @@
 
 class VarExportTest extends TestCase
 {
-    public function testCouldBeConstructedWithValueAsArgument()
-    {
-        new VarExport('aVal');
-    }
-
     /**
      * @dataProvider provideValues
-     *
-     * @param mixed $value
-     * @param mixed $expected
      */
     public function testShouldConvertValueToStringUsingVarExportFunction($value, $expected)
     {
diff --git a/Tests/fix_composer_json.php b/Tests/fix_composer_json.php
index bce1ebb..324f184 100644
--- a/Tests/fix_composer_json.php
+++ b/Tests/fix_composer_json.php
@@ -4,8 +4,8 @@
 
 $composerJson = json_decode(file_get_contents(__DIR__.'/../composer.json'), true);
 
-$composerJson['config']['platform']['ext-amqp'] = '1.7';
+$composerJson['config']['platform']['ext-amqp'] = '1.9.3';
 $composerJson['config']['platform']['ext-rdkafka'] = '3.3';
-$composerJson['config']['platform']['ext-gearman'] = '1.1';
+$composerJson['config']['platform']['ext-gearman'] = '2';
 
-file_put_contents(__DIR__.'/../composer.json', json_encode($composerJson, JSON_PRETTY_PRINT));
+file_put_contents(__DIR__.'/../composer.json', json_encode($composerJson, \JSON_PRETTY_PRINT));
diff --git a/Util/JSON.php b/Util/JSON.php
index f85738e..67411af 100644
--- a/Util/JSON.php
+++ b/Util/JSON.php
@@ -14,10 +14,7 @@ class JSON
     public static function decode($string)
     {
         if (!is_string($string)) {
-            throw new \InvalidArgumentException(sprintf(
-                'Accept only string argument but got: "%s"',
-                is_object($string) ? get_class($string) : gettype($string)
-            ));
+            throw new \InvalidArgumentException(sprintf('Accept only string argument but got: "%s"', is_object($string) ? $string::class : gettype($string)));
         }
 
         // PHP7 fix - empty string and null cause syntax error
@@ -26,32 +23,22 @@ public static function decode($string)
         }
 
         $decoded = json_decode($string, true);
-        if (JSON_ERROR_NONE !== json_last_error()) {
-            throw new \InvalidArgumentException(sprintf(
-                'The malformed json given. Error %s and message %s',
-                json_last_error(),
-                json_last_error_msg()
-            ));
+        if (\JSON_ERROR_NONE !== json_last_error()) {
+            throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg()));
         }
 
         return $decoded;
     }
 
     /**
-     * @param mixed $value
-     *
      * @return string
      */
     public static function encode($value)
     {
-        $encoded = json_encode($value, JSON_UNESCAPED_UNICODE);
-
-        if (JSON_ERROR_NONE !== json_last_error()) {
-            throw new \InvalidArgumentException(sprintf(
-                'Could not encode value into json. Error %s and message %s',
-                json_last_error(),
-                json_last_error_msg()
-            ));
+        $encoded = json_encode($value, \JSON_UNESCAPED_UNICODE);
+
+        if (\JSON_ERROR_NONE !== json_last_error()) {
+            throw new \InvalidArgumentException(sprintf('Could not encode value into json. Error %s and message %s', json_last_error(), json_last_error_msg()));
         }
 
         return $encoded;
diff --git a/Util/Stringify.php b/Util/Stringify.php
new file mode 100644
index 0000000..d8a48a8
--- /dev/null
+++ b/Util/Stringify.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Enqueue\Util;
+
+/**
+ * This is used to log message parts when in debug mode.
+ */
+class Stringify
+{
+    private $value;
+
+    public function __construct($value)
+    {
+        $this->value = $value;
+    }
+
+    public function __toString(): string
+    {
+        if (is_string($this->value) || is_scalar($this->value)) {
+            return $this->value;
+        }
+
+        return json_encode($this->value, \JSON_UNESCAPED_SLASHES);
+    }
+
+    public static function that($value): self
+    {
+        return new self($value);
+    }
+}
diff --git a/Util/VarExport.php b/Util/VarExport.php
index 4a48afa..9a91470 100644
--- a/Util/VarExport.php
+++ b/Util/VarExport.php
@@ -7,14 +7,8 @@
  */
 class VarExport
 {
-    /**
-     * @var mixed
-     */
     private $value;
 
-    /**
-     * @param mixed $value
-     */
     public function __construct($value)
     {
         $this->value = $value;
diff --git a/composer.json b/composer.json
index 638efea..c336c4b 100644
--- a/composer.json
+++ b/composer.json
@@ -3,41 +3,48 @@
     "type": "library",
     "description": "Message Queue Library",
     "keywords": ["messaging", "queue", "amqp", "rabbitmq"],
+    "homepage": "https://enqueue.forma-pro.com/",
     "license": "MIT",
     "require": {
-        "php": ">=5.6",
-        "queue-interop/queue-interop": "^0.6@dev",
-        "enqueue/null": "^0.8@dev",
-        "ramsey/uuid": "^2|^3.5",
-        "psr/log": "^1"
+        "php": "^8.1",
+        "queue-interop/amqp-interop": "^0.8.2",
+        "queue-interop/queue-interop": "^0.8",
+        "enqueue/null": "^0.10",
+        "enqueue/dsn": "^0.10",
+        "ramsey/uuid": "^3.5|^4",
+        "psr/log": "^1.0 || ^2.0 || ^3.0",
+        "psr/container": "^1.1 || ^2.0"
     },
     "require-dev": {
-        "phpunit/phpunit": "~5.5",
-        "symfony/console": "^2.8|^3",
-        "symfony/dependency-injection": "^2.8|^3",
-        "symfony/config": "^2.8|^3",
-        "symfony/event-dispatcher": "^2.8|^3",
-        "symfony/http-kernel": "^2.8|^3",
-        "enqueue/amqp-ext": "^0.8@dev",
-        "enqueue/amqp-lib": "^0.8@dev",
-        "enqueue/amqp-bunny": "^0.8@dev",
-        "enqueue/pheanstalk": "^0.8@dev",
-        "enqueue/gearman": "^0.8@dev",
-        "enqueue/rdkafka": "^0.8@dev",
-        "enqueue/dbal": "^0.8@dev",
-        "enqueue/fs": "^0.8@dev",
-        "enqueue/gps": "^0.8@dev",
-        "enqueue/redis": "^0.8@dev",
-        "enqueue/sqs": "^0.8@dev",
-        "enqueue/stomp": "^0.8@dev",
-        "enqueue/test": "^0.8@dev",
-        "enqueue/simple-client": "^0.8@dev",
-        "empi89/php-amqp-stubs": "*@dev"
+        "phpunit/phpunit": "^9.5",
+        "symfony/console": "^5.41|^6.0",
+        "symfony/dependency-injection": "^5.4|^6.0",
+        "symfony/config": "^5.4|^6.0",
+        "symfony/event-dispatcher": "^5.4|^6.0",
+        "symfony/http-kernel": "^5.4|^6.0",
+        "symfony/yaml": "^5.4|^6.0",
+        "enqueue/amqp-ext": "0.10.x-dev",
+        "enqueue/amqp-lib": "0.10.x-dev",
+        "enqueue/amqp-bunny": "0.10.x-dev",
+        "enqueue/pheanstalk": "0.10.x-dev",
+        "enqueue/gearman": "0.10.x-dev",
+        "enqueue/rdkafka": "0.10.x-dev",
+        "enqueue/dbal": "0.10.x-dev",
+        "enqueue/fs": "0.10.x-dev",
+        "enqueue/gps": "0.10.x-dev",
+        "enqueue/redis": "0.10.x-dev",
+        "enqueue/sqs": "0.10.x-dev",
+        "enqueue/stomp": "0.10.x-dev",
+        "enqueue/test": "0.10.x-dev",
+        "enqueue/simple-client": "0.10.x-dev",
+        "enqueue/mongodb": "0.10.x-dev",
+        "empi89/php-amqp-stubs": "*@dev",
+        "enqueue/dsn": "0.10.x-dev"
     },
     "suggest": {
-        "symfony/console": "^2.8|^3 If you want to use li commands",
-        "symfony/dependency-injection": "^2.8|^3",
-        "symfony/config": "^2.8|^3",
+        "symfony/console": "^5.4|^6.0 If you want to use cli commands",
+        "symfony/dependency-injection": "^5.4|^6.0",
+        "symfony/config": "^5.4|^6.0",
         "enqueue/amqp-ext": "AMQP transport (based on php extension)",
         "enqueue/stomp": "STOMP transport",
         "enqueue/fs": "Filesystem transport",
@@ -45,17 +52,28 @@
         "enqueue/dbal": "Doctrine DBAL transport",
         "enqueue/sqs": "Amazon AWS SQS transport"
     },
+    "support": {
+        "email": "opensource@forma-pro.com",
+        "issues": "https://github.com/php-enqueue/enqueue-dev/issues",
+        "forum": "https://gitter.im/php-enqueue/Lobby",
+        "source": "https://github.com/php-enqueue/enqueue-dev",
+        "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md"
+    },
     "autoload": {
         "psr-4": { "Enqueue\\": "" },
-        "files": ["functions_include.php"],
         "exclude-from-classmap": [
             "/Tests/"
         ]
     },
+    "config": {
+        "platform": {
+            "ext-amqp": "1.9.3"
+        }
+    },
     "minimum-stability": "dev",
     "extra": {
         "branch-alias": {
-            "dev-master": "0.8.x-dev"
+            "dev-master": "0.10.x-dev"
         }
     }
 }
diff --git a/functions.php b/functions.php
deleted file mode 100644
index fef445f..0000000
--- a/functions.php
+++ /dev/null
@@ -1,168 +0,0 @@
-<?php
-
-namespace Enqueue;
-
-use Enqueue\AmqpBunny\AmqpConnectionFactory as AmqpBunnyConnectionFactory;
-use Enqueue\AmqpExt\AmqpConnectionFactory as AmqpExtConnectionFactory;
-use Enqueue\AmqpLib\AmqpConnectionFactory as AmqpLibConnectionFactory;
-use Enqueue\Consumption\QueueConsumer;
-use Enqueue\Dbal\DbalConnectionFactory;
-use Enqueue\Fs\FsConnectionFactory;
-use Enqueue\Gearman\GearmanConnectionFactory;
-use Enqueue\Gps\GpsConnectionFactory;
-use Enqueue\Null\NullConnectionFactory;
-use Enqueue\Pheanstalk\PheanstalkConnectionFactory;
-use Enqueue\RdKafka\RdKafkaConnectionFactory;
-use Enqueue\Redis\RedisConnectionFactory;
-use Enqueue\Sqs\SqsConnectionFactory;
-use Enqueue\Stomp\StompConnectionFactory;
-use Interop\Queue\PsrConnectionFactory;
-use Interop\Queue\PsrContext;
-
-/**
- * @param string $dsn
- *
- * @return PsrConnectionFactory
- */
-function dsn_to_connection_factory($dsn)
-{
-    $map = [];
-
-    if (class_exists(FsConnectionFactory::class)) {
-        $map['file'] = FsConnectionFactory::class;
-    }
-
-    if (class_exists(AmqpExtConnectionFactory::class)) {
-        $map['amqp+ext'] = AmqpExtConnectionFactory::class;
-    }
-    if (class_exists(AmqpBunnyConnectionFactory::class)) {
-        $map['amqp+lib'] = AmqpLibConnectionFactory::class;
-    }
-    if (class_exists(AmqpLibConnectionFactory::class)) {
-        $map['amqp+bunny'] = AmqpBunnyConnectionFactory::class;
-    }
-
-    if (class_exists(AmqpExtConnectionFactory::class)) {
-        $map['amqp'] = AmqpExtConnectionFactory::class;
-    } elseif (class_exists(AmqpBunnyConnectionFactory::class)) {
-        $map['amqp'] = AmqpBunnyConnectionFactory::class;
-    } elseif (class_exists(AmqpLibConnectionFactory::class)) {
-        $map['amqp'] = AmqpLibConnectionFactory::class;
-    }
-
-    if (class_exists(NullConnectionFactory::class)) {
-        $map['null'] = NullConnectionFactory::class;
-    }
-
-    if (class_exists(DbalConnectionFactory::class)) {
-        $map['db2'] = DbalConnectionFactory::class;
-        $map['ibm_db2'] = DbalConnectionFactory::class;
-        $map['mssql'] = DbalConnectionFactory::class;
-        $map['pdo_sqlsrv'] = DbalConnectionFactory::class;
-        $map['mysql'] = DbalConnectionFactory::class;
-        $map['mysql2'] = DbalConnectionFactory::class;
-        $map['pdo_mysql'] = DbalConnectionFactory::class;
-        $map['pgsql'] = DbalConnectionFactory::class;
-        $map['postgres'] = DbalConnectionFactory::class;
-        $map['postgresql'] = DbalConnectionFactory::class;
-        $map['pdo_pgsql'] = DbalConnectionFactory::class;
-        $map['sqlite'] = DbalConnectionFactory::class;
-        $map['sqlite3'] = DbalConnectionFactory::class;
-        $map['pdo_sqlite'] = DbalConnectionFactory::class;
-    }
-
-    if (class_exists(GearmanConnectionFactory::class)) {
-        $map['gearman'] = GearmanConnectionFactory::class;
-    }
-
-    if (class_exists(PheanstalkConnectionFactory::class)) {
-        $map['beanstalk'] = PheanstalkConnectionFactory::class;
-    }
-
-    if (class_exists(RdKafkaConnectionFactory::class)) {
-        $map['kafka'] = RdKafkaConnectionFactory::class;
-        $map['rdkafka'] = RdKafkaConnectionFactory::class;
-    }
-
-    if (class_exists(RedisConnectionFactory::class)) {
-        $map['redis'] = RedisConnectionFactory::class;
-    }
-
-    if (class_exists(StompConnectionFactory::class)) {
-        $map['stomp'] = StompConnectionFactory::class;
-    }
-
-    if (class_exists(SqsConnectionFactory::class)) {
-        $map['sqs'] = SqsConnectionFactory::class;
-    }
-
-    if (class_exists(GpsConnectionFactory::class)) {
-        $map['gps'] = GpsConnectionFactory::class;
-    }
-
-    list($scheme) = explode(':', $dsn, 2);
-    if (false == $scheme || false === strpos($dsn, ':')) {
-        throw new \LogicException(sprintf('The scheme could not be parsed from DSN "%s"', $dsn));
-    }
-
-    if (false == array_key_exists($scheme, $map)) {
-        throw new \LogicException(sprintf(
-            'The scheme "%s" is not supported. Supported "%s"',
-            $scheme,
-            implode('", "', array_keys($map))
-        ));
-    }
-
-    $factoryClass = $map[$scheme];
-
-    return new $factoryClass($dsn);
-}
-
-/**
- * @param string $dsn
- *
- * @return PsrContext
- */
-function dsn_to_context($dsn)
-{
-    return dsn_to_connection_factory($dsn)->createContext();
-}
-
-/**
- * @param PsrContext $c
- * @param string     $topic
- * @param string     $body
- */
-function send_topic(PsrContext $c, $topic, $body)
-{
-    $topic = $c->createTopic($topic);
-    $message = $c->createMessage($body);
-
-    $c->createProducer()->send($topic, $message);
-}
-
-/**
- * @param PsrContext $c
- * @param string     $queue
- * @param string     $body
- */
-function send_queue(PsrContext $c, $queue, $body)
-{
-    $queue = $c->createQueue($queue);
-    $message = $c->createMessage($body);
-
-    $c->createProducer()->send($queue, $message);
-}
-
-/**
- * @param PsrContext $c
- * @param string     $queue
- * @param callable   $callback
- */
-function consume(PsrContext $c, $queue, callable $callback)
-{
-    $queueConsumer = new QueueConsumer($c);
-    $queueConsumer->bind($queue, $callback);
-
-    $queueConsumer->consume();
-}
diff --git a/functions_include.php b/functions_include.php
deleted file mode 100644
index cf5502a..0000000
--- a/functions_include.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-// Don't redefine the functions if included multiple times.
-if (false == function_exists('Enqueue\dsn_to_connection_factory')) {
-    require __DIR__.'/functions.php';
-}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 156ab49..69c12ca 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,16 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
-<phpunit backupGlobals="false"
-         backupStaticAttributes="false"
-         colors="true"
-         convertErrorsToExceptions="true"
-         convertNoticesToExceptions="true"
-         convertWarningsToExceptions="true"
-         processIsolation="false"
-         stopOnFailure="false"
-         syntaxCheck="false"
-         bootstrap="./vendor/autoload.php"
-        >
+<phpunit
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.5/phpunit.xsd"
+    colors="true"
+    bootstrap="./vendor/autoload.php"
+>
 
     <testsuites>
         <testsuite name="Message Queue Test Suite">
