{"id":79390,"date":"2026-04-03T12:37:04","date_gmt":"2026-04-03T07:07:04","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=79390"},"modified":"2026-04-22T11:28:23","modified_gmt":"2026-04-22T05:58:23","slug":"building-an-event-driven-system-in-drupal-with-subscribers-and-queue-processing","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/building-an-event-driven-system-in-drupal-with-subscribers-and-queue-processing\/","title":{"rendered":"Building an Event-Driven System in Drupal with Subscribers and Queue Processing"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>Modern applications are no longer built around doing everything in a single request. As systems grow, that approach quickly becomes slow, hard to maintain, and difficult to scale. Drupal gives you powerful tools to solve this problem using an event-driven approach combined with background processing via queues.<\/p>\n<p>In this article, we\u2019ll walk through a practical implementation of this pattern using a custom module, and more importantly, understand why it works so well.<\/p>\n<h2>Overview<\/h2>\n<p>We built a custom Drupal module where:<\/p>\n<ul>\n<li>\u00a0Creating an article triggers an event<\/li>\n<li>\u00a0A subscriber listens to that event<\/li>\n<li>\u00a0Logic is applied (like priority detection)<\/li>\n<li>\u00a0Data is pushed to a queue<\/li>\n<li>\u00a0A queue worker processes it in the background<\/li>\n<\/ul>\n<h2>The Problem with Traditional Flow<\/h2>\n<p>In a typical setup, everything happens during the same request:<\/p>\n<ul>\n<li>User submits content<\/li>\n<li>System processes data<\/li>\n<li>External APIs are called<\/li>\n<li>Logs are written<\/li>\n<li>Notifications are sent<\/li>\n<\/ul>\n<p>All of this increases response time. If any step is slow or fails, the entire request is affected.<br \/>\nThis is where event-driven architecture and queues make a big difference.<\/p>\n<h2>Module Structure<\/h2>\n<p>modules\/custom\/article_intelligence\/<br \/>\n\u2502<br \/>\n\u251c\u2500\u2500 article_intelligence.info.yml<br \/>\n\u251c\u2500\u2500 article_intelligence.module<br \/>\n\u251c\u2500\u2500 article_intelligence.services.yml<br \/>\n\u2502<br \/>\n\u251c\u2500\u2500 src\/<br \/>\n\u2502 \u251c\u2500\u2500 Event\/<br \/>\n\u2502 \u2502 \u2514\u2500\u2500 ArticleCreatedEvent.php<br \/>\n\u2502 \u2502<br \/>\n\u2502 \u251c\u2500\u2500 EventSubscriber\/<br \/>\n\u2502 \u2502 \u2514\u2500\u2500 ArticleSubscriber.php<br \/>\n\u2502 \u2502<br \/>\n\u2502 \u2514\u2500\u2500 Plugin\/<br \/>\n\u2502 \u2514\u2500\u2500 QueueWorker\/<br \/>\n\u2502 \u2514\u2500\u2500 ArticleQueueWorker.php<\/p>\n<h2>Step 1: Module Info File<\/h2>\n<p><em>File Path: article_intelligence.info.yml<\/em><\/p>\n<pre>name: Article Intelligence\r\ntype: module\r\ndescription: Event-driven article processing system\r\ncore_version_requirement: ^10 || ^11\r\npackage: Custom<\/pre>\n<h2>Step 2: Trigger Event on Article Create<\/h2>\n<p>We use hook_entity_insert() to detect when content is created.<br \/>\n<em>File Path: article_intelligence.module<\/em><\/p>\n<pre>&lt;?php\r\nuse Drupal\\node\\NodeInterface;\r\nuse Drupal\\article_intelligence\\Event\\ArticleCreatedEvent;\r\n\r\n\/**\r\n\u00a0* Implements hook_entity_insert().\r\n\u00a0*\/\r\nfunction article_intelligence_entity_insert($entity) {\r\n\u00a0 \/\/ Check if the inserted entity is an article node then fire article created event.\r\n\u00a0 if ($entity instanceof NodeInterface &amp;&amp; $entity-&gt;bundle() === 'article') {\r\n\u00a0 \u00a0 \\Drupal::logger('article_intelligence')-&gt;info('Article created, Now firing event.');\r\n    \/\/ Fire and dispatch custom event to trigger subscribers.\r\n    $event = new ArticleCreatedEvent($entity);\r\n\u00a0 \u00a0 \\Drupal::service('event_dispatcher')-&gt;dispatch($event, ArticleCreatedEvent::EVENT_NAME);\r\n\u00a0 }\r\n}<\/pre>\n<h2>Step 3: Create Custom Event<\/h2>\n<p><em>File Path: \/src\/Event\/ArticleCreatedEvent.php<\/em><\/p>\n<pre>&lt;?php\r\n\r\nnamespace Drupal\\article_intelligence\\Event;\r\n\r\nuse Symfony\\Contracts\\EventDispatcher\\Event;\r\nuse Drupal\\node\\NodeInterface;\r\n\r\nclass ArticleCreatedEvent extends Event {\r\n  \/\/ Define a event name.\r\n\u00a0 const EVENT_NAME = 'article_intelligence.article_created';\r\n  \/\/ Store the node object for use in subscribers.\r\n\u00a0 protected $node;\r\n\u00a0 \/**\r\n\u00a0 \u00a0* Constructor to initialize the event with the node.\r\n\u00a0 \u00a0*\r\n\u00a0 \u00a0* @param NodeInterface $node\r\n\u00a0 \u00a0* \u00a0 The article node that was created.\r\n  \u00a0*\/\r\n\u00a0 public function __construct(NodeInterface $node) {\r\n\u00a0 \u00a0 $this-&gt;node = $node;\r\n\u00a0 }\r\n  \/**\r\n\u00a0 \u00a0* Getter for the node object.\r\n\u00a0 \u00a0*\r\n\u00a0 \u00a0* @return NodeInterface\r\n\u00a0 \u00a0* \u00a0 The article node associated with this event.\r\n\u00a0 \u00a0*\/\r\n\u00a0 public function getNode() {\r\n\u00a0 \u00a0 return $this-&gt;node;\r\n\u00a0 }\r\n}<\/pre>\n<h2>Step 4: Register Event Subscriber<\/h2>\n<p><em>File Path: article_intelligence.services.yml<\/em><\/p>\n<pre>services:\r\n\u00a0 article_intelligence.subscriber:\r\n\u00a0 \u00a0 class: Drupal\\article_intelligence\\EventSubscriber\\ArticleSubscriber\r\n\u00a0 \u00a0 arguments: ['@logger.factory', '@queue']\r\n\u00a0 \u00a0 tags:\r\n\u00a0 \u00a0 \u00a0 - { name: event_subscriber }<\/pre>\n<h2>Step 5: Event Subscriber Logic<\/h2>\n<ul>\n<li>Read article data<\/li>\n<li>Decide priority<\/li>\n<li>Push to queue<\/li>\n<\/ul>\n<p><em>File Path: \/src\/EventSubscriber\/ArticleSubscriber.php<\/em><\/p>\n<pre>&lt;?php\r\n\r\nnamespace Drupal\\article_intelligence\\EventSubscriber;\r\n\r\nuse Symfony\\Component\\EventDispatcher\\EventSubscriberInterface;\r\nuse Drupal\\article_intelligence\\Event\\ArticleCreatedEvent;\r\nuse Psr\\Log\\LoggerInterface;\r\nuse Drupal\\Core\\Queue\\QueueFactory;\r\nuse Symfony\\Component\\DependencyInjection\\ContainerInterface;\r\nuse Drupal\\Core\\Logger\\LoggerChannelFactoryInterface;\r\n\r\nclass ArticleSubscriber implements EventSubscriberInterface {\r\n\r\n\u00a0 \/**\r\n\u00a0 \u00a0* Logger service.\r\n\u00a0 \u00a0*\r\n\u00a0 \u00a0* @var \\Psr\\Log\\LoggerInterface\r\n\u00a0 \u00a0*\/\r\n\u00a0 protected $logger;\r\n\r\n\u00a0 \/**\r\n\u00a0 \u00a0* Queue service.\r\n\u00a0 \u00a0*\r\n\u00a0 \u00a0* @var \\Drupal\\Core\\Queue\\QueueInterface\r\n\u00a0 \u00a0*\/\r\n\u00a0 protected $queue;\r\n\r\n\u00a0 \/**\r\n\u00a0 \u00a0* Constructor with dependency injection.\r\n\u00a0 \u00a0*\/\r\n\u00a0 public function __construct(LoggerChannelFactoryInterface $logger_factory, QueueFactory $queue_factory) {\r\n\u00a0 \u00a0 $this-&gt;logger = $logger_factory-&gt;get('article_intelligence');\r\n\u00a0 \u00a0 $this-&gt;queue = $queue_factory-&gt;get('article_intelligence_queue');\r\n\u00a0 }\r\n\r\n\u00a0 \/**\r\n\u00a0 \u00a0* Subscribes to the Article Created event.\r\n\u00a0 \u00a0*\/\r\n\u00a0 public static function getSubscribedEvents() {\r\n\u00a0 \u00a0 return [\r\n\u00a0 \u00a0 \u00a0 ArticleCreatedEvent::EVENT_NAME =&gt; 'onArticleCreated',\r\n\u00a0 \u00a0 ];\r\n\u00a0 }\r\n\r\n\u00a0 \/**\r\n\u00a0 \u00a0* Handles the Article Created event.\r\n\u00a0 \u00a0*\/\r\n\u00a0 public function onArticleCreated(ArticleCreatedEvent $event) {\r\n\r\n\u00a0 \u00a0 \/\/ Get the node from the event and analyze its title.\r\n\u00a0 \u00a0 $node = $event-&gt;getNode();\r\n\u00a0 \u00a0 $title = strtolower($node-&gt;getTitle());\r\n\r\n\u00a0 \u00a0 \/\/Here we can add any logic like calling external api, analyzing content, etc. For simplicity, we will just check for keywords in the title to determine priority. \r\n\u00a0 \u00a0 $priority = 'normal';\r\n\u00a0 \u00a0 if (strpos($title, 'urgent') !== FALSE || strpos($title, 'breaking') !== FALSE) {\r\n\u00a0 \u00a0 \u00a0 $priority = 'high';\r\n\u00a0 \u00a0 }\r\n\r\n\u00a0 \u00a0 \/\/ Log priority using injected logger service.\r\n\u00a0 \u00a0 $this-&gt;logger-&gt;info('Priority assigned: @p', ['@p' =&gt; $priority]);\r\n\r\n\u00a0 \u00a0 \/\/ Push data to queue using injected queue service.\r\n\u00a0 \u00a0 $this-&gt;queue-&gt;createItem([\r\n\u00a0 \u00a0 \u00a0 'nid' =&gt; $node-&gt;id(),\r\n\u00a0 \u00a0 \u00a0 'priority' =&gt; $priority,\r\n\u00a0 \u00a0 ]);\r\n\u00a0 }\r\n\r\n}<\/pre>\n<h2>Step 6: Create Queue Worker<\/h2>\n<p>This processes queued data in the background.<br \/>\n<em>File Path: \/src\/Plugin\/QueueWorker\/ArticleQueueWorker.php<\/em><\/p>\n<pre>&lt;?php\r\n\r\nnamespace Drupal\\article_intelligence\\Plugin\\QueueWorker;\r\n\r\nuse Drupal\\Core\\Queue\\QueueWorkerBase;\r\nuse Drupal\\Core\\Plugin\\ContainerFactoryPluginInterface;\r\nuse Symfony\\Component\\DependencyInjection\\ContainerInterface;\r\nuse GuzzleHttp\\ClientInterface;\r\nuse Psr\\Log\\LoggerInterface;\r\n\r\n\/**\r\n\u00a0* Processes items for Article Intelligence Queue.\r\n\u00a0*\r\n\u00a0* @QueueWorker(\r\n\u00a0* \u00a0 id = \"article_intelligence_queue\",\r\n\u00a0* \u00a0 title = @Translation(\"Article Intelligence Queue\"),\r\n\u00a0* \u00a0 cron = {\"time\" = 60}\r\n\u00a0* )\r\n\u00a0* Cron will process this queue for up to 60 seconds per run.\r\n\u00a0*\/\r\nclass ArticleQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface {\r\n\r\n\u00a0 \/**\r\n\u00a0 \u00a0* HTTP client service.\r\n\u00a0 \u00a0*\r\n\u00a0 \u00a0* @var \\GuzzleHttp\\ClientInterface\r\n\u00a0 \u00a0*\/\r\n\u00a0 protected $httpClient;\r\n\r\n\u00a0 \/**\r\n\u00a0 \u00a0* Logger service.\r\n\u00a0 \u00a0*\r\n\u00a0 \u00a0* @var \\Psr\\Log\\LoggerInterface\r\n\u00a0 \u00a0*\/\r\n\u00a0 protected $logger;\r\n\r\n\u00a0 \/**\r\n\u00a0 \u00a0* Constructor with DI.\r\n\u00a0 \u00a0*\/\r\n\u00a0 public function __construct(array $configuration, $plugin_id, $plugin_definition, ClientInterface $http_client, LoggerInterface $logger) {\r\n\u00a0 \u00a0 parent::__construct($configuration, $plugin_id, $plugin_definition);\r\n\r\n\u00a0 \u00a0 $this-&gt;httpClient = $http_client;\r\n\u00a0 \u00a0 $this-&gt;logger = $logger;\r\n\u00a0 }\r\n\r\n\u00a0 \/**\r\n\u00a0 \u00a0* Creates instance using container.\r\n\u00a0 \u00a0*\/\r\n\u00a0 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {\r\n\u00a0 \u00a0 return new static(\r\n\u00a0 \u00a0 \u00a0 $configuration,\r\n\u00a0 \u00a0 \u00a0 $plugin_id,\r\n\u00a0 \u00a0 \u00a0 $plugin_definition,\r\n\u00a0 \u00a0 \u00a0 $container-&gt;get('http_client'),\r\n\u00a0 \u00a0 \u00a0 $container-&gt;get('logger.factory')-&gt;get('article_intelligence')\r\n\u00a0 \u00a0 );\r\n\u00a0 }\r\n\r\n\u00a0 \/**\r\n\u00a0 \u00a0* Processes a single queue item.\r\n\u00a0 \u00a0*\/\r\n\u00a0 public function processItem($data) {\r\n\u00a0 \u00a0 try {\r\n\u00a0 \u00a0 \u00a0 \/**\r\n\u00a0 \u00a0 \u00a0 \u00a0* HTTP Client (External API Call)\r\n\u00a0 \u00a0 \u00a0 \u00a0* Used to communicate with external systems.\r\n\u00a0 \u00a0 \u00a0 \u00a0* Currently using a dummy API for testing.\r\n\u00a0 \u00a0 \u00a0 \u00a0*\r\n\u00a0 \u00a0 \u00a0 \u00a0* Example real-world use cases:\r\n\u00a0 \u00a0 \u00a0 \u00a0* - Send article data to external API\r\n\u00a0 \u00a0 \u00a0 \u00a0* - Trigger notification system\r\n\u00a0 \u00a0 \u00a0 \u00a0* - Integrate with AI\/third-party services\r\n\u00a0 \u00a0 \u00a0 \u00a0*\/\r\n\u00a0 \u00a0 \u00a0 $response = $this-&gt;httpClient-&gt;get('https:\/\/jsonplaceholder.typicode.com\/posts\/1', [\r\n\u00a0 \u00a0 \u00a0 \u00a0 'timeout' =&gt; 5,\r\n\u00a0 \u00a0 \u00a0 ]);\r\n\r\n\u00a0 \u00a0 \u00a0 $result = json_decode($response-&gt;getBody()-&gt;getContents(), TRUE);\r\n\r\n\u00a0 \u00a0 \u00a0 \/\/ Log success.\r\n\u00a0 \u00a0 \u00a0 $this-&gt;logger-&gt;info(\r\n\u00a0 \u00a0 \u00a0 \u00a0 'API call successful for article ID: @nid | Response title: @title',\r\n\u00a0 \u00a0 \u00a0 \u00a0 [\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 '@nid' =&gt; $data['nid'],\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 '@title' =&gt; $result['title'] ?? 'N\/A',\r\n\u00a0 \u00a0 \u00a0 \u00a0 ]\r\n\u00a0 \u00a0 \u00a0 );\r\n\r\n\u00a0 \u00a0 \u00a0 \/**\r\n\u00a0 \u00a0 \u00a0 \u00a0* Priority-based processing\r\n\u00a0 \u00a0 \u00a0 \u00a0* You can apply different logic based on priority.as of now we are just logging high priority articles.\r\n\u00a0 \u00a0 \u00a0 \u00a0*\r\n\u00a0 \u00a0 \u00a0 \u00a0* Example real-world use cases:\r\n\u00a0 \u00a0 \u00a0 \u00a0* - Notification API call\r\n\u00a0 \u00a0 \u00a0 \u00a0* - Batch processing\r\n\u00a0 \u00a0 \u00a0 \u00a0*\/\r\n\u00a0 \u00a0 \u00a0 if ($data['priority'] === 'high') {\r\n\u00a0 \u00a0 \u00a0 \u00a0 $this-&gt;logger-&gt;warning(\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 'High priority article processed: @nid',\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 ['@nid' =&gt; $data['nid']]\r\n\u00a0 \u00a0 \u00a0 \u00a0 );\r\n\u00a0 \u00a0 \u00a0 }\r\n\r\n\u00a0 \u00a0 }\r\n\u00a0 \u00a0 catch (\\Exception $e) {\r\n\u00a0 \u00a0 \u00a0 \/\/ Log error.\r\n\u00a0 \u00a0 \u00a0 $this-&gt;logger-&gt;error(\r\n\u00a0 \u00a0 \u00a0 \u00a0 'API request failed for article ID: @nid | Error: @message',\r\n\u00a0 \u00a0 \u00a0 \u00a0 [\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 '@nid' =&gt; $data['nid'],\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 '@message' =&gt; $e-&gt;getMessage(),\r\n\u00a0 \u00a0 \u00a0 \u00a0 ]\r\n\u00a0 \u00a0 \u00a0 );\r\n\r\n\u00a0 \u00a0 \u00a0 \/\/ Re-throw so item can be retried.\r\n\u00a0 \u00a0 \u00a0 throw $e;\r\n\u00a0 \u00a0 }\r\n\u00a0 }\r\n\r\n}<\/pre>\n<h2>Step 7: Test the Flow<\/h2>\n<ul>\n<li>Create a new article<\/li>\n<li>Use title that have keyword like: &#8220;urgent, breaking&#8221;<\/li>\n<li>Run queue &#8211; <strong>drush queue:run article_intelligence_queue<\/strong><\/li>\n<li>Check logs<\/li>\n<li>You will see &#8211; <strong>Processing Node ID: 5 with priority: high<\/strong><\/li>\n<\/ul>\n<h2>Conclusion<\/h2>\n<p>The combination of events, subscribers, and queues in Drupal creates a powerful pattern that makes your system faster, more scalable, and easier to maintain. By moving heavy processing to the background, you keep the user experience smooth while also making it easier to extend and evolve your application in the future.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction Modern applications are no longer built around doing everything in a single request. As systems grow, that approach quickly becomes slow, hard to maintain, and difficult to scale. Drupal gives you powerful tools to solve this problem using an event-driven approach combined with background processing via queues. In this article, we\u2019ll walk through a [&hellip;]<\/p>\n","protected":false},"author":1708,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":6},"categories":[3602],"tags":[4357,5030],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/79390"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/1708"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=79390"}],"version-history":[{"count":6,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/79390\/revisions"}],"predecessor-version":[{"id":79404,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/79390\/revisions\/79404"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=79390"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=79390"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=79390"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}