<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Dmitrii Tarasov on Medium]]></title>
        <description><![CDATA[Stories by Dmitrii Tarasov on Medium]]></description>
        <link>https://medium.com/@dmitrii_tarasov?source=rss-5f3bf3d13c56------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*uVhpBcuD8Ivevy7A905Gkw.jpeg</url>
            <title>Stories by Dmitrii Tarasov on Medium</title>
            <link>https://medium.com/@dmitrii_tarasov?source=rss-5f3bf3d13c56------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 08:46:56 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@dmitrii_tarasov/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Observability with RabbitMQ and microservices]]></title>
            <link>https://medium.com/@dmitrii_tarasov/observability-with-rabbitmq-and-microservices-6fc5d3a22e41?source=rss-5f3bf3d13c56------2</link>
            <guid isPermaLink="false">https://medium.com/p/6fc5d3a22e41</guid>
            <category><![CDATA[distributed-tracing]]></category>
            <category><![CDATA[rabbitmq]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[symfony-messenger]]></category>
            <category><![CDATA[microservices]]></category>
            <dc:creator><![CDATA[Dmitrii Tarasov]]></dc:creator>
            <pubDate>Mon, 30 Aug 2021 21:14:05 GMT</pubDate>
            <atom:updated>2021-08-30T21:14:05.416Z</atom:updated>
            <content:encoded><![CDATA[<p>When using RabbitMQ and microservices, how do you gain observability of a system? In this article I will describe how we achieved:</p><ul><li>Permanent events log and replay functionality;</li><li>Publishers/consumers topology visibility;</li><li>Distributed tracing of events.</li></ul><p>In <a href="https://medium.com/@dmitrii_tarasov/symfony-messenger-in-microservices-message-contracts-and-resilience-b9cc1a95fcf9">the previous article</a> I described how we organize messages contracts, communication-layer, and resilience in PHP microservices. The current article is more language-agnostic, as well as it is just a collection of ideas/approaches rather than call for a specific implementation.</p><h3>Events history</h3><p>In AMQP once a message is delivered (and optionally acknowledged), it is removed from a queue, and you will never see it again. What if you need to investigate what happened at that stage?</p><p>I can think of several options, for example:</p><ul><li>Enabling debug level of logging in message bus.</li><li>RabbitMQ <a href="https://www.rabbitmq.com/firehose.html">Firehose</a>.</li><li>Subscribing to all messages and storing/redirecting them to separate storage or message bus (might be done via custom subscriber or via tools like Apache Flink, Kafka RabbitMQ connector, etc.).</li></ul><p>We have chosen the last option, which works the following way:</p><ul><li>There is a queue, which is subscribed to all messages in topic exchange via “#” binding key (which means matching any routing key);</li><li>There are consumers, which constantly read messages from that queue and store it in the appropriate format to persistent storage.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*iiGle1YhdFnz7WzCTo1jVQ.png" /><figcaption>Logger subscribed to all messages</figcaption></figure><p>I should also mention that the main disadvantage of such approach is that the replication/logging queue will receive duplicates of every message in the cluster, which might affect performance of this cluster. This might be not a big deal, but at least proper monitoring is required. You should be alerted immediately if some of the publishers go crazy and starts generating an enormous amount of messages (which in my opinion is a must-have anyway).</p><p>Having storage of all events, we got:</p><ul><li>Events history log, which can be used for errors investigation (what was in the message which triggered that error?);</li><li>Events, which might be a source of data for your data warehouse and reporting;</li><li>Ability to replay messages.</li></ul><h3>Events replay</h3><p>The latter (replay functionality) is especially useful in situations when something breaks, and data for previous X minutes/days/etc. must be reprocessed.</p><p>Another example is: we start building a pricing application, which needs to have information about prices for all the time (what was offered?) and orders (what was actually sold?) so that it can learn, predict and advise on prices. No problem, we can replay all offers/orders messages for it!</p><p>Replay required custom implementation, though it is low-hanging fruit. Though you must be careful implementing such functionality because republishing messages towards all consumers of this message might be harmful, normally it should be done into a specific queue only. This is achievable by using direct exchange and publishing to a routing key that matches desired queue name.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*NMi_r74GWyOTB6DKmJAQBg.png" /><figcaption>Republish to a specific queue</figcaption></figure><h3>Topology visibility</h3><p>Normally configuration of publishers and consumers is standardized and enforced by a library that is used for RabbitMQ communication. In our case we had a slightly adjusted configuration of standard Symfony Messenger, which looks like this:</p><pre>transports:<br>    amqp_product_category_v1:<br>        dsn: &#39;%env(MESSENGER_TRANSPORT_AMQP_DSN)%&#39;<br>        options:<br>            queue_name: &#39;%queue_name.category.v1%&#39;<br>            binding_keys: [ &#39;%product.category.v1.routing_key%&#39; ]<br>    amqp_product_v1:<br>        dsn: &#39;%env(MESSENGER_TRANSPORT_AMQP_DSN)%&#39;<br>        options:<br>            publish_routing_key: &#39;%product.v1.routing_key%&#39;<br>routing:<br>    <em># routing: Message -&gt; Transports (multiple allowed)<br>    </em>&#39;App\Message\ProductMessage&#39;: [ &#39;amqp_product_v1&#39; ]</pre><p>One of our engineers implemented a nice tool that analyzes such configuration across multiple applications and aggregates this data into the possible topology of RabbitMQ communication, which might look so for example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5s38grHJJQVuaM9nOiOquw.png" /><figcaption>How auto-generated topology might look like</figcaption></figure><h3>Tracing</h3><p>Having visualized configuration is great, but this is “possible theoretical” topology, which does not reflect actual data flow (which might be different in different environments because of versions of software, or which might not reflect the fact that some of the publishers are not generating messages for whatever reason for example).</p><p>Additionally, data flow creates a dependent chain of events, for example, 2 products were damaged in a warehouse (published by warehouse application), which triggered stock level decrease by 2 (published by inventory application), which triggered a decrease in the quantity offered to customer (published by offer application), which … (and so on).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8X9MZwvaG1Snk52IziTBbA.png" /><figcaption>Possible chain of events</figcaption></figure><p>And now when you want to investigate why customer sees less available quantity, you should be able to trace this chain back to the event of products being damaged.</p><p>Both problems (real data flow and tracing back chain of events) might be solved by implementing distributed tracing, which however requires an ability to link messages to each other and identify producers and consumers.</p><p>This is where messages metadata gets very helpful:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7Adhlbk6R3j_6OW_raWL9Q.png" /><figcaption>Messages metadata explained</figcaption></figure><p>When pushing this data to a tracing system, you would see the visualized chain of events that happened in your system. Google Trace is shown as an example in the screenshot below, but any other solution would work as well.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BUBQXzh1MecUC8c_ooKJ1w.png" /><figcaption>Example of messages links visualization in Google Trace</figcaption></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6fc5d3a22e41" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Symfony Messenger in microservices: message contracts and resilience]]></title>
            <link>https://medium.com/@dmitrii_tarasov/symfony-messenger-in-microservices-message-contracts-and-resilience-b9cc1a95fcf9?source=rss-5f3bf3d13c56------2</link>
            <guid isPermaLink="false">https://medium.com/p/b9cc1a95fcf9</guid>
            <category><![CDATA[symfony]]></category>
            <category><![CDATA[php]]></category>
            <category><![CDATA[microservices]]></category>
            <category><![CDATA[rabbitmq]]></category>
            <category><![CDATA[symfony-messenger]]></category>
            <dc:creator><![CDATA[Dmitrii Tarasov]]></dc:creator>
            <pubDate>Thu, 19 Aug 2021 00:09:44 GMT</pubDate>
            <atom:updated>2021-08-30T21:16:00.787Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hq_I8n3cMciIUwR6" /><figcaption>Photo by <a href="https://unsplash.com/@jack_anstey?utm_source=medium&amp;utm_medium=referral">Jack Anstey</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Which libraries do you use when building communication between PHP microservices via message bus? Though this task does not sound uncommon, we ended up with custom implementation on top of Symfony Messenger component. In this article I will try to share the most important concepts of our in-house solution.</p><h3>What did we try to solve?</h3><p>While building our ERP system (a kind of mix of supply chain management, order processing, and accounting) we followed Spotify team model, where a team is split into smaller squads and tribes, which aim to be as autonomous as possible. This (according to Conway’s law) led to implementation of technical solution trying to reflect independence of teams, such as:</p><ul><li>Microservices architecture, where each service has a single owner-team.</li><li>Continuous delivery avoiding centralized deployment trains.</li><li>Contract-driven communication between services (and teams in turn as well).</li><li>Prevailing of asynchronous (message bus) over synchronous (RPC/REST/etc.) communication.</li></ul><p>In addition to the directions given above, there were few more tasks to solve regarding asynchronous communication:</p><ul><li>RabbitMQ was already used, so it needed to be adapted first. But in general, we wanted to be as agnostic as possible in terms of using message buses. We ended up using our solution with at least AMQP, SQS and DB transports, so the goal was achieved.</li><li>There must be a way to enforce contracts of messages with a framework for cross-team reviews of these contracts.</li><li>Software must be resilient.</li></ul><p>After analysis, we came up to the conclusion that Symfony Messenger gives the most flexibility in terms of being message bus agnostic. It provides a unified wrapper, where transports (Redis, AMQP, DB, AWS SQS, etc.) are pluggable as separate dependencies.</p><p>Though there are also significant limitations of Symfony Messenger (when I write this article, Symfony 5.3 is the latest stable version):</p><ul><li>The component is not really adopted to work with messages published and consumed by different applications, see <a href="https://github.com/symfony/symfony/issues/33912">#33912</a> for example.</li><li>There are gaps in resilience behavior, e.g. see open <a href="https://github.com/symfony/symfony/issues/36870">#36870</a>, already resolved <a href="https://github.com/symfony/symfony/pull/36864">#36864</a>, and a few more.</li></ul><p>Therefore, we decided to build our custom transport (as a private composer package), which I am going to describe below.</p><h3>Implementation</h3><p>This transport is easy to use, one needs only to install the private package and specify a custom prefix in transport DSN instead of standard “amqp://”.</p><p>The transport was built on top of <a href="https://github.com/symfony/amqp-messenger">symfony/amqp-messenger</a>, so it reuses connection classes, while sender and receiver are extended.</p><h4>#1 Deserialization and contracts</h4><p>Having custom transport allows introducing additional configuration options without breaking already existing options for standard “amqp-messenger”.</p><pre>dsn: &#39;custom-amqp://...&#39;<br>options:<br>    serialization:<br>        deserialize_to: &#39;App\Message\ProductMessage&#39;<br>    validation:<br>        schema_path: &#39;product_message_schema.json&#39;<br>        validate_on_consume: true</pre><p>First, we needed to create a way to support multiple applications’ communication, therefore we introduced “deserialize_to” option, which tells the library which class must be used as a message object. In native Symfony Messenger deserialization happens based on message header, which contains full class name (including namespace) of initial message — which is of course hardly usable when publisher and subscriber are different applications.</p><p>Second, we wanted to enforce contract validation (we use JSON messages, therefore JSON-schema is a contract). Just having a contract is not enough, messages should be actually validated against this contract. But when should you apply it: on publish or on consume? Our reasoning was:</p><ul><li>Validation is obligatory on publishing, because publisher is the owner of a contract — they should avoid publishing invalid messages at any cost.</li><li>However, validation is optional on consumption and is disabled by default.</li></ul><h4>#2 Contracts organization</h4><p>All right, we have enforced teams to use contracts in messages. Now no message can be published without a contract. We all also agreed that contracts should be maintained in a backward-compatible manner: if a change is incompatible, new major version or just another contract should be introduced.</p><p>But now we have multiple messages contracts and JSON files circulating through multiple applications with unclear versioning and vague ownership and maintenance process.</p><p>In addition to that many of our JSON-schema contracts contained sub-schemas like this:</p><pre>&quot;price&quot;: {<br>    &quot;description&quot;: &quot;Promotion price&quot;,<br>    &quot;$ref&quot;: &quot;../common/money.json&quot;<br>},</pre><p>Where “money.json” contains an object consisting of currency and amount, which is reused and copied multiple times.</p><p>We came up with the following solution:</p><p>1. All contracts are stored in a single repository:</p><ul><li>This allows introducing a process of cross-team review for any contract changes, e.g. just an agreement or enforcement of merge-request reviews.</li><li>This allows setting up notifications for any contract changes(e.g. if notifications are sent to a dedicated Slack channel).</li><li>This solves a problem of sub-schemas, which now are described once and referenced multiple times. No copy-pasting is needed anymore.</li></ul><p>2. Every time a change happens, contracts are automatically packed into a new version of PHP package (Symfony bundle in this case), which then can be installed/updated in publishers/consumers applications.</p><ul><li>This solves versioning problem: applications rely on specific contracts, which might be updated only explicitly.</li><li>There is no limitation towards a specific technology, we might build similar package for Java/JavaScript/Python, etc.</li></ul><p>3. Contracts also contain some metadata, e.g. description of what this message is about and destination (in terminology of RabbitMQ this is equal to “routing key”).</p><p>Additionally, such approach allowed auto-generation of documentation pages with a human-readable catalog of all data flowing through message bus in the system.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/582/1*0J2ofhM883UJHXqWco6e5g.png" /><figcaption>Example of messages contracts documentation</figcaption></figure><p>Symfony bundle containing contracts exposes parameters, which can be referenced as schema path or routing key, so no path or routing keys hardcoding is needed anymore, no manual contracts copying is needed as well.</p><pre>options:<br>    serialization:<br>        deserialize_to: &#39;App\Message\ProductMessage&#39;<br>    validation:<br>        schema_path: &#39;%contracts.schema.product.path%&#39;<br>    publish_routing_key: &#39;%contracts.product.v1.routing_key%&#39;</pre><h4>#3 Messages metadata</h4><p>We found it very useful to inject metadata into each message, because it greatly helps with logging, debugging, and tracing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/782/1*iSoYHsfVBdTJ7OxImqsRHg.png" /><figcaption>Example of message body with metadata</figcaption></figure><p>This might be a subject for a separate article since the topic is not too small.</p><h4>#4 Expected unexpected</h4><p>Okay, what do we do when failures happen? Symfony Messenger already has a built-in retries mechanism and failure transports concept, which can be configured for consumers.</p><p>What about producers? What happens if RabbitMQ is temporary not available, or message schema validation fails? Our goal is to avoid losing data at any cost. Unfortunately, there is nothing available out of the box, so we had to implement adding to failed transport on publishing as well. Symfony Messenger has a great concept of message stamps, so we distinguish failed on publishing and failed on consuming using custom stamps added to envelopes.</p><p>It was important for us to continuously monitor failed messages (the aim is to have zero of them), because some of them might be republished/reconsumed automatically, while others need manual resolution. We monitor number of failed messages via Prometheus and Grafana, where applications expose monitoring endpoint, which contains multiple metrics including failed messages.</p><p>Finally, what should engineers do with failed messages? Symfony Messenger has CLI commands to manage them (list/view/reconsume/delete), which we extended with “republish” action. But CLI has a disadvantage as well: it means engineers should get SSH/kubectl exec/whatever else permission in production, which is kind of complicated when you work with sensitive information (e.g., customer orders info). So, we additionally implemented HTTP endpoints and UI with proper permissions restrictions for failed messages management.</p><p>See also second part: <a href="https://medium.com/@dmitrii_tarasov/observability-with-rabbitmq-and-microservices-6fc5d3a22e41">Observability with RabbitMQ and microservices</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b9cc1a95fcf9" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>