<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thomas P</title>
    <description>The latest articles on DEV Community by Thomas P (@scullwm).</description>
    <link>https://dev.to/scullwm</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F486271%2F23e53327-2051-463a-8444-880f14dabcc6.jpeg</url>
      <title>DEV Community: Thomas P</title>
      <link>https://dev.to/scullwm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/scullwm"/>
    <language>en</language>
    <item>
      <title>A badly written ticket can beat a well-written one</title>
      <dc:creator>Thomas P</dc:creator>
      <pubDate>Tue, 15 Sep 2026 21:03:20 +0000</pubDate>
      <link>https://dev.to/scullwm/a-badly-written-ticket-can-beat-a-well-written-one-2ig5</link>
      <guid>https://dev.to/scullwm/a-badly-written-ticket-can-beat-a-well-written-one-2ig5</guid>
      <description>&lt;h2&gt;
  
  
  When a Ready Ticket Is No Longer Ready
&lt;/h2&gt;

&lt;p&gt;We spend a lot of time discussing what makes a good ticket: clear acceptance criteria, enough context, useful links, a decent description.&lt;/p&gt;

&lt;p&gt;But there is another factor we rarely talk about: &lt;strong&gt;age&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A ticket created last week can be surprisingly short and still be perfectly usable. The team remembers the discussion that created it, the problem is still fresh, and a lot of context doesn't need to be written down because everyone already shares it.&lt;/p&gt;

&lt;p&gt;Take the exact same ticket and leave it in the backlog for six months.&lt;/p&gt;

&lt;p&gt;The text hasn't changed, but its quality probably has.&lt;/p&gt;

&lt;p&gt;Maybe the product evolved. Maybe the code around it changed. Maybe priorities shifted. Maybe the person who created it is no longer on the team. Even when none of those things happened, people simply forget the conversations and assumptions that originally surrounded the ticket.&lt;/p&gt;

&lt;p&gt;This is something I discuss in &lt;em&gt;&lt;a href="https://www.amazon.com/dp/B0GTX7KF62" rel="noopener noreferrer"&gt;Tech Refinement&lt;/a&gt;&lt;/em&gt;: &lt;strong&gt;context decays&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And it creates an interesting situation. A beautifully structured six-month-old ticket can actually be less useful than a three-line ticket created yesterday. One looks better on paper, but the other still has its context attached to it.&lt;/p&gt;

&lt;p&gt;That's why I think stale tickets deserve a slightly different treatment during refinement.&lt;/p&gt;

&lt;p&gt;When an old ticket comes back to the top of the backlog, the first question shouldn't immediately be: &lt;em&gt;How are we going to implement this?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It should be: &lt;strong&gt;Is this still what we want?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And then: &lt;strong&gt;Has anything changed since this was written?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes the answer is no, and you continue normally. Sometimes the ticket needs to be updated. Sometimes it needs another conversation with Product. And occasionally, you realize that the best thing to do is simply close it.&lt;/p&gt;

&lt;p&gt;I've seen a team refine a ticket that had been sitting in the backlog for months, only to discover before implementation that priorities had changed and the work wasn't needed anymore. The refinement itself wasn't bad. The mistake was assuming that an old decision was still a current one.&lt;/p&gt;

&lt;p&gt;A ticket being “ready” is not a permanent property.&lt;/p&gt;

&lt;p&gt;It was ready &lt;strong&gt;given the context that existed when it was refined&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The older that context becomes, the more reasonable it is to challenge it again.&lt;/p&gt;

&lt;p&gt;You don't need a new process, an expiration date in Jira, or a rule saying that every ticket older than three months must be rewritten.&lt;/p&gt;

&lt;p&gt;Just treat age as a signal.&lt;/p&gt;

&lt;p&gt;Before spending engineering time figuring out &lt;em&gt;how&lt;/em&gt; to build an old ticket, make sure the &lt;em&gt;what&lt;/em&gt; and the &lt;em&gt;why&lt;/em&gt; are still alive too.&lt;/p&gt;

</description>
      <category>agile</category>
      <category>productivity</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>AB tests with Symfony 5 made easy</title>
      <dc:creator>Thomas P</dc:creator>
      <pubDate>Fri, 09 Oct 2020 17:16:15 +0000</pubDate>
      <link>https://dev.to/scullwm/ab-tests-with-symfony-5-made-easy-1km4</link>
      <guid>https://dev.to/scullwm/ab-tests-with-symfony-5-made-easy-1km4</guid>
      <description>&lt;p&gt;The company I work for is a data-driven company. It is necessary for us to make the best decisions and aim for our core value: being customer-centric.&lt;/p&gt;

&lt;p&gt;Naturally, when we iterate on a feature, we study the data to define how to improve it. And because we can’t improve what we don’t measure, we measure the results of our new version with the previous version.&lt;/p&gt;

&lt;p&gt;To do this, we almost systematically perform AB tests that we couple with our events database.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AB tests are not for experimenting things, but to measure them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And since it's &lt;a href="https://hacktoberfest.digitalocean.com/"&gt;HacktoberFest&lt;/a&gt;, we published one of our tools in open source here: &lt;a href="https://github.com/travaux-com/VariantRetriever/"&gt;Travaux.com VariantRetriever&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next part of this article is to start your first AB test, like changing the subject of an email when a new user registers.&lt;br&gt;
The experiment we are doing will be called &lt;code&gt;welcome-email-experiment&lt;/code&gt; and it will have a &lt;code&gt;control&lt;/code&gt; version (the actual version) and a &lt;code&gt;participant&lt;/code&gt; (or variant) version (the new version we want to try).&lt;/p&gt;

&lt;p&gt;When we will send this email, we’re going to require the VariantRetriever and ask it: for this experiment and this user identifier, tell me which variant it should have.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lets run a new AB test with Symfony 5
&lt;/h2&gt;

&lt;p&gt;First step is of course to install the AB test package in your project:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;composer require travaux-com/variantretriever&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Because it’s not a Symfony bundle, you don’t need to declare it in your AppKernel file, but you still need (recommanded in fact) to declare it as a service.&lt;/p&gt;

&lt;p&gt;We’re going to declare it as a service with the Symfony container factory system to have our “variant retriever” with all information about our running AB tests. (&lt;code&gt;welcome-email-experiment&lt;/code&gt; and &lt;code&gt;display-cta&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;App\FeatureFlag\VariantRetriever&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;factory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@Travaux\VariantRetriever\Factory\VariantRetrieverFactory'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;createVariantRetriever'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;welcome-email-experiment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;control-email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;participant-email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;display-cta&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;control-cta&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;75&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;participant-cta&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The service name is actually &lt;code&gt;App\FeatureFlag\VariantRetriever&lt;/code&gt; but I strongly encourage you to customize it.&lt;/p&gt;

&lt;p&gt;Now you’re able to use the &lt;a href="https://symfony.com/doc/current/service_container/autowiring.html"&gt;Symfony autowiring&lt;/a&gt; to retrieve the VariantRetrieverInterface and use it in your command handler.&lt;br&gt;
And so on, how to use it ?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$affectedVariant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$variantRetriever&lt;/span&gt;
           &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getVariantForExperiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my-ab-test'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
           &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;VariantRetrieverInterface&lt;/code&gt; actually came with the getVariantForExperiment method where you will have to define the experiment you want to test (by just filling the experiment name) and the “user identifier” who just need to be a string, so it can be a UUID v4 or an auto-incremented integer.&lt;/p&gt;

&lt;p&gt;It will return you a &lt;code&gt;Variant&lt;/code&gt; value object that contains the name of the affected variant.&lt;/p&gt;

&lt;p&gt;Now you can imagine select the right translation key to use depending on the affected variant for your email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$experimentTranslationsKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
       &lt;span class="s1"&gt;'control'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'email.welcome.subject_control'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="s1"&gt;'variant'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'email.welcome.subject_variant'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$userVariant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$variantRetriever&lt;/span&gt;
           &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getVariantForExperiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'welcome-email-experiment'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
           &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$emailSubject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$experimentTranslationsKeys&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$userVariant&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This package will ensure you to always retrieve the same variant for your user, without any database or cache.&lt;/p&gt;

&lt;p&gt;You’re now able to run your first AB in your Symfony project 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Go beyond this example
&lt;/h2&gt;

&lt;p&gt;As you can see, we’ve use the &lt;a href="https://symfony.com/doc/current/service_container/factories.html"&gt;Symfony container factory&lt;/a&gt; system to instantiate our VariantRetriever, but it can also be declared with &lt;a href="https://symfony.com/doc/current/service_container/calls.html"&gt;Symfony method call&lt;/a&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="s"&gt;Travaux\VariantRetriever\Retriever\VariantRetrieverInterface&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Travaux\VariantRetriever\Retriever\VariantRetriever&lt;/span&gt;
        &lt;span class="na"&gt;calls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;addExperiment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!service&lt;/span&gt;
                    &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Travaux\VariantRetriever\ValueObject\Experiment&lt;/span&gt;
                    &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;welcome-email-experiment'&lt;/span&gt;
                        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!service&lt;/span&gt;
                            &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Travaux\VariantRetriever\ValueObject\Variant&lt;/span&gt;
                            &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;control'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;100&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!service&lt;/span&gt;
                            &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Travaux\VariantRetriever\ValueObject\Variant&lt;/span&gt;
                            &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;variant'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h1&gt;
  
  
  How does it work internally ?
&lt;/h1&gt;

&lt;p&gt;Thanks to &lt;a href="https://www.php.net/manual/en/function.crc32.php"&gt;php crc32&lt;/a&gt; function, we’re able to process a string that return everytime the same integer value. So on, even if it’s a big number, we’re able to reduce it under a range of 0 to 99 that match the rollout percentage given by one of our variant.&lt;/p&gt;

&lt;p&gt;Also, the &lt;code&gt;Travaux\VariantRetriever\Retriever\VariantRetriever&lt;/code&gt; class is not final to allow you to extend it. Like injecting a PsrLogger and/or the event dispatcher to keep a trace of which experiment have been affected to which user. You can also add you own method to retrieve a variant based on your User entity object like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getUserVariantForExperiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;Experiment&lt;/span&gt; &lt;span class="nv"&gt;$experiment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Feel Free to contribute to this project or give feedback.&lt;/p&gt;

</description>
      <category>php</category>
      <category>symfony</category>
      <category>abtest</category>
    </item>
  </channel>
</rss>
