<?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: Berlin Tech Blog</title>
    <description>The latest articles on DEV Community by Berlin Tech Blog (@berlin-tech-blog).</description>
    <link>https://dev.to/berlin-tech-blog</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.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1445%2Ffb63a047-7074-4b9d-a5d9-1a8d9cbe3b2b.png</url>
      <title>DEV Community: Berlin Tech Blog</title>
      <link>https://dev.to/berlin-tech-blog</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/berlin-tech-blog"/>
    <language>en</language>
    <item>
      <title>Automate Your Java Upgrades: A Practical Case Study with OpenRewrite and GitHub Actions</title>
      <dc:creator>Daniil Roman</dc:creator>
      <pubDate>Fri, 10 Oct 2025 13:12:02 +0000</pubDate>
      <link>https://dev.to/berlin-tech-blog/automate-your-java-upgrades-a-practical-case-study-with-openrewrite-and-github-actions-3od7</link>
      <guid>https://dev.to/berlin-tech-blog/automate-your-java-upgrades-a-practical-case-study-with-openrewrite-and-github-actions-3od7</guid>
      <description>&lt;p&gt;Tech debt grows relentlessly. Even on services you don't touch, dependencies become outdated, creating a constant maintenance burden. Manually upgrading dozens of services is slow and error-prone. What if you could automate a significant part of that process?&lt;/p&gt;

&lt;p&gt;Remember your last big Spring Boot or Java version upgrade? How did it go? Did you spend hours renaming &lt;code&gt;javax&lt;/code&gt; to &lt;code&gt;jakarta&lt;/code&gt; packages across 30 or maybe 50 different services? Or perhaps you lost a whole day figuring out why a simple dependency bump broke the build? If any of this sounds familiar, you're in the right place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is OpenRewrite?
&lt;/h2&gt;

&lt;p&gt;OpenRewrite is an open-source refactoring engine that automates code modifications at scale, enabling consistent and reliable refactoring across large codebases and significantly reducing manual effort.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;OpenRewrite works by making changes to &lt;a href="https://docs.openrewrite.org/concepts-and-explanations/lossless-semantic-trees" rel="noopener noreferrer"&gt;Lossless Semantic Trees&lt;/a&gt; (LSTs) that represent your source code and printing the modified trees back into source code. You can then review the changes in your code and commit the results. Modifications to the LST are performed in &lt;a href="https://docs.openrewrite.org/concepts-and-explanations/visitors" rel="noopener noreferrer"&gt;Visitors&lt;/a&gt; and visitors are aggregated into &lt;a href="https://docs.openrewrite.org/concepts-and-explanations/recipes" rel="noopener noreferrer"&gt;Recipes&lt;/a&gt;. OpenRewrite recipes make minimally invasive changes to your source code that honor the original formatting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can check this project out on &lt;a href="https://github.com/openrewrite/rewrite" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4yzc37ojz6j023vw1rqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4yzc37ojz6j023vw1rqx.png" alt="OpenRewrite statistics on GitHub" width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When would you use OpenRewrite?
&lt;/h2&gt;

&lt;p&gt;You might be thinking, "This sounds great in theory, but what are the real-world use cases?" Let's recall some notable migrations many of us have faced recently:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spring Boot 2.x to 3.x migration&lt;/strong&gt;&lt;br&gt;
This migration was especially remarkably by the seemingly simple task of renaming &lt;code&gt;javax.*&lt;/code&gt; to &lt;code&gt;jakarta.*&lt;/code&gt; namespaces. However, this change had to be applied to every single service using Spring Boot. In a typical Java and Spring ecosystem, that means changing it everywhere. &lt;/p&gt;

&lt;p&gt;OpenRewrite offers a recipe that automates this entire process.&lt;br&gt;
&lt;a href="https://docs.openrewrite.org/recipes/java/spring/boot3/upgradespringboot_3_0" rel="noopener noreferrer"&gt;https://docs.openrewrite.org/recipes/java/spring/boot3/upgradespringboot_3_0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JUnit 4 to JUnit 5 Migration&lt;/strong&gt; &lt;br&gt;
Imagine you need to migrate from JUnit 4 to JUnit 5, but your codebase still has a few outdated annotations scattered around. As a part of this migration you'd need to rename &lt;code&gt;@BeforeClass&lt;/code&gt; to &lt;code&gt;@BeforeAll&lt;/code&gt; or &lt;code&gt;@AfterClass&lt;/code&gt; to &lt;code&gt;@AfterAll&lt;/code&gt;. &lt;br&gt;
It doesn't sound too complicated, but it's tedious work that can be fully automated with an OpenRewrite recipe. &lt;br&gt;
&lt;a href="https://docs.openrewrite.org/running-recipes/popular-recipe-guides/migrate-from-junit-4-to-junit-5" rel="noopener noreferrer"&gt;https://docs.openrewrite.org/running-recipes/popular-recipe-guides/migrate-from-junit-4-to-junit-5&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Java Version Upgrades:&lt;/strong&gt; &lt;br&gt;
Or maybe you're upgrading to Java 21 and want to replace the deprecated &lt;code&gt;new URL(String)&lt;/code&gt; constructor with the &lt;code&gt;URI.create(String).toURL()&lt;/code&gt; across your entire codebase. &lt;br&gt;
There's a recipe for that too: &lt;a href="https://docs.openrewrite.org/recipes/java/migrate/upgradetojava21" rel="noopener noreferrer"&gt;https://docs.openrewrite.org/recipes/java/migrate/upgradetojava21&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In other words, we developers are constantly challenged with keeping our dependencies up to date. OpenRewrite is here to rescue us—or at least, to significantly reduce the pain.&lt;/p&gt;
&lt;h2&gt;
  
  
  Our experience of using OpenRewrite
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What worked for us
&lt;/h3&gt;

&lt;p&gt;Let's start with what went well, as we found the tool both useful and promising.&lt;/p&gt;
&lt;h4&gt;
  
  
  Keeping &lt;code&gt;pom.xml&lt;/code&gt; in shape
&lt;/h4&gt;

&lt;p&gt;In the OpenRewrite ecosystem, the magic comes from its recipes. For us, the first no-brainer was the &lt;a href="http://docs.openrewrite.org/recipes/maven/bestpractices" rel="noopener noreferrer"&gt;Apache Maven best practices&lt;/a&gt; recipe.&lt;br&gt;
It was immediately clear that we had no other tool in our stack that could consistently keep our &lt;code&gt;pom.xml&lt;/code&gt; files in good shape.&lt;/p&gt;

&lt;p&gt;As a simple but welcome feature, this recipe reorders the sections of a &lt;code&gt;pom.xml&lt;/code&gt; to follow a standard pattern. This helps with readability, especially in large files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4kmgn45dsdpr5ah0m5r6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4kmgn45dsdpr5ah0m5r6.png" alt="pom.xml reordering result" width="800" height="681"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But its real power lies elsewhere: the recipe can find and remove duplicate or unused dependencies, improving the long-term stability of a service. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfm9ns4i3f83mox52ec4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfm9ns4i3f83mox52ec4.png" alt="Unused or duplicated dependencies were removed" width="800" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As a note of caution, I admit it can be scary at first to accept an automated PR that removes dependencies. &lt;strong&gt;Make sure you have good test coverage&lt;/strong&gt; before trusting any automated tool to this extent.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;
  
  
  Refactoring test libraries
&lt;/h5&gt;

&lt;p&gt;Of course, we wanted to see it work on actual Java code. As always, the safest place to try a new tool is on your tests, so that's exactly what we did. We were already in the process of standardizing on AssertJ, so we introduced three relevant recipes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;org.openrewrite.java.testing.assertj.Assertj&lt;/li&gt;
&lt;li&gt;org.openrewrite.java.testing.mockito.MockitoBestPractices&lt;/li&gt;
&lt;li&gt;org.openrewrite.java.testing.testcontainers.TestContainersBestPractices
We ran these without specific expectations and were pleasantly surprised when they spotted and fixed several sore spots in our test code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqi9ods6f1pyb8y1pqzi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqi9ods6f1pyb8y1pqzi.png" alt="Static import was fixed" width="800" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrwygleopwk62rdbhz0s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrwygleopwk62rdbhz0s.png" alt="The result of a recipe for testcontainers to use a specific Docker image" width="800" height="156"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  What didn't work
&lt;/h4&gt;

&lt;p&gt;But of course, the juicy part is always where things go wrong.&lt;br&gt;
In the following paragraphs, we will look at a few examples that we were not entirely satisfied with. &lt;/p&gt;
&lt;h5&gt;
  
  
  Complex, custom refactoring
&lt;/h5&gt;

&lt;p&gt;We tried to use a recipe to fully migrate our tests from Hamcrest to AssertJ, but it simply ignored our custom matchers. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3spex3ti9u7kcngo5gpz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3spex3ti9u7kcngo5gpz.png" alt="Complex Hamcrest matcher that OpenRewrite recipe wasn't able to migrate to AssertJ" width="800" height="739"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While some recipes are more powerful than others, our general feeling is that OpenRewrite struggles with highly complex or bespoke refactorings on its own.&lt;/p&gt;
&lt;h5&gt;
  
  
  Running Java recipes on Kotlin projects
&lt;/h5&gt;

&lt;p&gt;It may seem obvious that Java recipes should only be run on Java projects. However, like many companies, we have a mix of Java and Kotlin projects, so we simply ran the recipes against all of our team's services to see what would happen. It turns out that it &lt;em&gt;partially&lt;/em&gt; works, but it fails in enough cases to produce strange changes and broken PRs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuux72pcmc9rhdd1raixo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuux72pcmc9rhdd1raixo.png" alt="A result of running a Java recipe against Kotlin project" width="800" height="127"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This makes things tricky if you want to run a uniform set of recipes across all repositories using a tool like GitHub Actions, which we'll cover next.&lt;/p&gt;
&lt;h5&gt;
  
  
  Newest recipes are often commercial
&lt;/h5&gt;

&lt;p&gt;This might be obvious if you're already familiar with OpenRewrite, but it's worth mentioning. If you want to use OpenRewrite for Spring Boot upgrades, you'll find that the recipe for the latest version might be under a commercial license. For example, if Spring Boot 3.5 is the latest release, the open-source recipe might only support up to version 3.4. This makes perfect sense from a business perspective, but it's something to keep in mind. In short: the OpenRewrite &lt;em&gt;engine&lt;/em&gt; is open-source, but the most cutting-edge &lt;em&gt;recipes&lt;/em&gt; are often licensed separately.&lt;/p&gt;
&lt;h2&gt;
  
  
  Our automation setup with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;There are a few ways to run OpenRewrite recipes. If you're using Maven, you can add the &lt;code&gt;rewrite-maven-plugin&lt;/code&gt; directly to your &lt;code&gt;pom.xml&lt;/code&gt;. This can be configured to run during your local build or only on CI.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;project&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;build&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;plugins&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.openrewrite.maven&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;rewrite-maven-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;6.18.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;exportDatatables&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/exportDatatables&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;activeRecipes&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;recipe&amp;gt;&lt;/span&gt;org.openrewrite.staticanalysis.JavaApiBestPractices&lt;span class="nt"&gt;&amp;lt;/recipe&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/activeRecipes&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.openrewrite.recipe&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;rewrite-static-analysis&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.17.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/plugins&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/build&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, you can run the plugin directly from the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn &lt;span class="nt"&gt;-U&lt;/span&gt; org.openrewrite.maven:rewrite-maven-plugin:run &lt;span class="nt"&gt;-Drewrite&lt;/span&gt;.recipeArtifactCoordinates&lt;span class="o"&gt;=&lt;/span&gt;org.openrewrite.recipe:rewrite-static-analysis:RELEASE &lt;span class="nt"&gt;-Drewrite&lt;/span&gt;.activeRecipes&lt;span class="o"&gt;=&lt;/span&gt;org.openrewrite.staticanalysis.JavaApiBestPractices &lt;span class="nt"&gt;-Drewrite&lt;/span&gt;.exportDatatables&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the flexibility to run it manually, as part of a CI pipeline, or on a nightly schedule. &lt;br&gt;
We chose the third option: &lt;strong&gt;run it via a scheduled GitHub Action on a daily basis and automatically create a PR if any changes are detected.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why not just use the Maven plugin?
&lt;/h3&gt;

&lt;p&gt;The main drawback of adding the plugin to your &lt;code&gt;pom.xml&lt;/code&gt; is that it significantly slows down every build, even when there are no changes to be made. You could run it as a CI-only check, but that creates a frustrating workflow: the CI build would fail, and a developer would have to run the command locally to generate the changes and push another commit. This kind of friction hurts tool adoption.&lt;/p&gt;

&lt;p&gt;Running OpenRewrite on a schedule mimics the behavior of Dependabot or Renovate. Developers don't have to actively run anything; they simply review and merge the auto-generated PRs. Ideally, only a small portion of these PRs will have failures.&lt;/p&gt;

&lt;p&gt;Another benefit of the GitHub Action approach is centralization. We can update the recipes in a single, shared workflow file and have that change apply to all our repositories without touching a single &lt;code&gt;pom.xml&lt;/code&gt;. And since the result is always a PR and not a direct commit, it's a completely safe operation.&lt;/p&gt;
&lt;h3&gt;
  
  
  The GitHub workflow in detail
&lt;/h3&gt;

&lt;p&gt;Imagine you have 20 repositories. Modifying the &lt;code&gt;pom.xml&lt;/code&gt; in every one of them just to add or change a recipe would be painful and would quickly lead to abandoning the tool. With a centralized GitHub Action, however, each repository only needs a small trigger file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OpenRewrite Scheduled PR&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;MON-FRI'&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Allows manual triggering&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;call-openrewrite-workflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-organisation/your-repository/.github/workflows/reusable-openrewrite-auto-pr.yml@main&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file references a &lt;strong&gt;reusable workflow&lt;/strong&gt;, which contains the actual logic and OpenRewrite configuration. Now, whenever we add or remove a recipe, we only modify the central workflow, and all repositories pick up the change on their next scheduled run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reusable OpenRewrite Auto PR Workflow&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ADDITIONAL_MVN_COMMAND_TO_APPLY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Additional&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;OpenRewrite&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;command&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;apply'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;PR_BRANCH_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openrewrite/auto-improvements&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;check-branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check if branch exists&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;should_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.check_branch.outputs.should_run }}&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check if branch exists&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;check_branch&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if git ls-remote --heads origin ${{ env.PR_BRANCH_NAME }} | grep -q ${{ env.PR_BRANCH_NAME }}; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "Branch ${{ env.PR_BRANCH_NAME }} already exists. Skipping workflow."&lt;/span&gt;
            &lt;span class="s"&gt;echo "should_run=false" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;echo "Branch does not exist. Proceeding with workflow."&lt;/span&gt;
            &lt;span class="s"&gt;echo "should_run=true" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

  &lt;span class="na"&gt;openrewrite&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Apply OpenRewrite recommendations&lt;/span&gt;

    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;check-branch&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.check-branch.outputs.should_run == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Java&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;temurin'&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;21'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;maven'&lt;/span&gt;
          &lt;span class="na"&gt;settings-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workspace }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run OpenRewrite via Maven&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;./mvnw --batch-mode -U org.openrewrite.maven:rewrite-maven-plugin:run \&lt;/span&gt;
            &lt;span class="s"&gt;-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-testing-frameworks:RELEASE \&lt;/span&gt;
            &lt;span class="s"&gt;-Drewrite.activeRecipes=org.openrewrite.staticanalysis.JavaApiBestPractices,org.openrewrite.maven.BestPractices&lt;/span&gt;

          &lt;span class="s"&gt;${{ inputs.ADDITIONAL_MVN_COMMAND_TO_APPLY }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create Pull Request&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create_pr&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;peter-evans/create-pull-request@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;commit-message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refactor:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;apply&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;OpenRewrite&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;recommendations"&lt;/span&gt;
          &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refactor:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;apply&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;OpenRewrite&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;recommendations"&lt;/span&gt;
          &lt;span class="na"&gt;add-paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;.&lt;/span&gt;
            &lt;span class="s"&gt;:!settings.xml&lt;/span&gt;
            &lt;span class="s"&gt;:!toolchains.xml&lt;/span&gt;
          &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;This Pull Request was automatically generated by OpenRewrite.&lt;/span&gt;

            &lt;span class="s"&gt;## Changes Applied&lt;/span&gt;
            &lt;span class="s"&gt;The following recipes were applied:&lt;/span&gt;
            &lt;span class="s"&gt;- `org.openrewrite.maven.BestPractices` (Maven best practices)&lt;/span&gt;
            &lt;span class="s"&gt;- `org.openrewrite.staticanalysis.JavaApiBestPractices` (Java API best practices)&lt;/span&gt;
            &lt;span class="s"&gt;${{ inputs.OPENREWRITE_RECIPES_TO_APPLY }}&lt;/span&gt;

            &lt;span class="s"&gt;Please review the changes and merge if acceptable.&lt;/span&gt;
          &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.PR_BRANCH_NAME }}&lt;/span&gt;
          &lt;span class="na"&gt;delete-branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;openrewrite&lt;/span&gt;
            &lt;span class="s"&gt;automated-pr&lt;/span&gt;
          &lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PR Status&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "Pull request creation status: ${{ steps.create_pr.outputs.pull-request-operation }}"&lt;/span&gt;
          &lt;span class="s"&gt;echo "Pull request number: ${{ steps.create_pr.outputs.pull-request-number }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: &lt;br&gt;
Several unrelated and infrastructure-specific steps have been removed from the GitHub workflow described above.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Below you can see the steps of the GitHub workflow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhbhnr0zx5sa19b9uey0z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhbhnr0zx5sa19b9uey0z.png" alt="OpenRewrite GitHub workflow steps" width="800" height="905"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The main steps are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run OpenRewrite via Maven&lt;/li&gt;
&lt;li&gt;Create Pull Request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach has proven to be robust; we've already added new recipes and removed old ones, confirming it works well in different scenarios. You can scope a shared GitHub Action to a team or an entire organization and still provide repository-specific overrides using environment variables. This allows the shared workflow to be as complex as necessary, as long as it remains maintainable.&lt;/p&gt;

&lt;h4&gt;
  
  
  Our results
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;We ran &lt;strong&gt;5 distinct recipes&lt;/strong&gt; on a schedule across our team's repositories.&lt;/li&gt;
&lt;li&gt;The workflow was rolled out to &lt;strong&gt;14 services&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In a short time, we have already merged over &lt;strong&gt;40 automated PRs&lt;/strong&gt; generated by this system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjw8iw4rzl3zoldt0fsbt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjw8iw4rzl3zoldt0fsbt.png" alt="PR description of a successful run of an OpenRewrite GitHub workflow" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;In this article, we covered the pain points OpenRewrite solves, what worked (and didn't work) for us, and how we use the open-source version with GitHub Actions to run recipes automatically across all our repositories.&lt;/p&gt;

&lt;p&gt;So far, our experience with OpenRewrite has been very positive. It has filled a crucial gap in our toolkit, especially for keeping our Maven &lt;code&gt;pom.xml&lt;/code&gt; files clean and consistent.&lt;/p&gt;

&lt;p&gt;Check out the OpenRewrite documentation to find a recipe that fits your needs, and feel free to use our GitHub workflow as inspiration for your own automation.&lt;/p&gt;

</description>
      <category>java</category>
      <category>openrewrite</category>
      <category>githubactions</category>
      <category>techdebt</category>
    </item>
    <item>
      <title>Conversations That Mattered: My Journey Mentoring a Senior into Leadership</title>
      <dc:creator>Kleinanzeigen &amp; mobile.de</dc:creator>
      <pubDate>Thu, 11 Sep 2025 19:42:12 +0000</pubDate>
      <link>https://dev.to/berlin-tech-blog/conversations-that-mattered-my-journey-mentoring-a-senior-into-leadership-2p4m</link>
      <guid>https://dev.to/berlin-tech-blog/conversations-that-mattered-my-journey-mentoring-a-senior-into-leadership-2p4m</guid>
      <description>&lt;p&gt;an article by &lt;a href="https://www.linkedin.com/in/gmaldonadol/" rel="noopener noreferrer"&gt;Gonzalo Maldonado&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://www.mobile.de/careers/" rel="noopener noreferrer"&gt;mobile.de&lt;/a&gt; we run a mentoring program where people can sign up as mentors or mentees. In my role as Engineering Manager, which I’ve been doing for several years at different companies and where I focus on supporting teams and individual growth, I volunteered as a mentor and was paired with a Senior Backend Developer — and I was excited to see what would come next.&lt;/p&gt;

&lt;p&gt;My current mentorship relationship began, as many do: we created a calendar invite and then we introduced ourselves. I learned she was a Senior Backend Developer and that she was looking for some professional development guidance. Her CV looked solid to me and she came with a lot of questions, which I was more than happy to answer based on my past experience. I felt really excited, while I was really nervous. Would I be enough to meet her expectations? What are her expectations? Would I be able to help her grow in the way she needed? . She wasn’t sure whether Tech Lead or Principal Engineer was the right next step, and that uncertainty shaped our work: both paths demand technical excellence, yes, but also excellent communication, organisational influence, and the confidence to make tough decisions.&lt;/p&gt;

&lt;p&gt;I would like to share with you my experience on this short but enriching journey, as I feel it didn’t only help my mentee to grow professionally and personally, but also it helped me a lot reflecting on how a good mentoring relationship and program should look like, how to adapt when the context differs from my previous mentoring experiences, how cultural background affects the way we mentor and how to communicate for getting the best outcomes.&lt;/p&gt;

&lt;p&gt;My aim in this post is quite simple: to offer no more than an inspirational, practical guide for any mentor who wants to be part of such a journey and to show that you don’t need a specific role in the company to be an effective mentor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial steps
&lt;/h2&gt;

&lt;p&gt;At the beginning we met to get to know each other and validate whether our mentoring relationship would potentially be a good match or not. During that first conversation we agreed on the goals we wanted to achieve and discussed what each of us could contribute to the process. Spoiler alert: We turned out to be a great match.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff821f2y2fnsm85x70g3p.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff821f2y2fnsm85x70g3p.jpg" alt=" " width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We also wanted to give this a proper structure and for this reason we defined weekly 1:1 sessions of 30 minutes.&lt;/p&gt;

&lt;p&gt;Her aims were twofold: explore the Tech Lead and Principal Engineer paths, and improve communication so she could build trust and influence, so in the future she could potentially take one of these two roles and just because it is nice to build trust. Now it was time to start doing.&lt;/p&gt;

&lt;p&gt;Learning style was also part of the conversation, as I needed to know whether she preferred reading or watching videos. Since she was more of a reading person, I defined a weekly cadence for sharing written resources which we would later discuss in our 1:1s. Always in a timely manner, as she was going to need enough time to read and reflect about the topic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Growth Areas
&lt;/h2&gt;

&lt;p&gt;Once we recognised what we wanted to talk about, we created a brief roadmap of the topics we wanted to discuss in our weekly meetings:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building confidence in your own abilities&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Confidence is the foundation of day-to-day work, and because I’ve seen impostor syndrome become a major issue — I suffer from it myself — we addressed it first, although we returned to the topic several times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building trust across the team&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trust isn’t only self-confidence — it’s confidence from others: peers, managers, and cross-functional partners. How can you feel confident if you do not have a proper relationship with others? And how can you build such a relationship and trust? This is tough, but we talked about strategies on how to overcome this situation. At the end of the day it is all about talking and setting up the right expectations, as we’re here to have a good relationship and act as a team and not only like individuals. If there is no trust, your impact will be limited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Organisational influence and communication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you trust yourself and your team, the next step is influencing the broader organisation. I’m not talking about being the rock-star in the spotlight all the time; I mean being an example to other engineers and an inspirational contributor, whether you’re a leader or not. For example, a senior engineer might lead cross-team architecture reviews, build a reusable internal library, or run recurring tech talks — small, tangible actions that set an example, spread best practices, and align technical work with product impact. By doing that, we can develop the best approaches together and generate ideas that help us make the most of our product and how we work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to keep yourself motivated when things are not going well?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We all know that being 100% motivated all the time is kind of impossible, because life happens and because we’re just human beings, this is where keeping the motivation on a good level when things are not going well is tough and a few things tend to help: celebrate small victories, reset yourself, zoom out to see the bigger picture and please please, don’t blame yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to handle conflicts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I’m still developing my skills in this area, and this experience taught me the most: by listening to my mentee’s perspective on conflict, I discovered concrete ways to improve my communication and decision-making. What I confirmed here is that communication and decision making are crucial here, but at the same time please always give yourself the time to hear both parties, to understand where they’re coming from and try to apply techniques for addressing these situations. You don’t have any techniques for it? Then I think it is crucial for you to start building your own conflict management toolset, it will just make your life easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self development and self knowledge&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From beginning to end, it was a process of self‑discovery. How do we give feedback to ourselves and to others? How do we receive feedback from others, and what do we do with it? These were some of the questions we discussed in our sessions. Too often you take feedback and don’t turn it into action, which can cause you to miss opportunities. Feedback from others is a key piece of the puzzle — and I don’t mean just from upper management; I mean the people you work with every day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engineering Ladders&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;People talk about “engineering ladders” a lot, but they rarely explain what it actually takes to move up. The higher you go, the fuzzier the criteria become — which makes promotions feel mysterious and unfair. This is where the &lt;a href="https://www.engineeringladders.com/" rel="noopener noreferrer"&gt;Engineering Ladders framework&lt;/a&gt; comes into play and what I really like about it, is that it will give a bit more of a concrete vision on which are the dimensions considered for developers, tech leads and engineering managers — even PMs are part of this framework. Once you read it, you will understand that the dimensions are pretty well defined. We liked the most that they don’t only talk about technical skills, which is also important, but that there are more dimensions which are also relevant. Use this as one of many tools for building your career path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical skills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Skipping this wasn’t an option, right? Definitely not. We focused on the topics mentioned above because that was the path my mentee wanted to follow — she was already confident in her technical skills. That said, technical ability is not something to ignore, and we worked to enhance her skills further to support the influence work I described earlier.&lt;/p&gt;

&lt;p&gt;First, we discussed how well she knew the product, the architecture, testing approaches, resiliency, and so on. With that context, we created a plan to review the application’s big picture and identify improvements to make it more resilient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical decisions and decision making&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the technical solution is clear, some implementations seem “easy enough.” But what happens when they aren’t? When you must choose between “quick and dirty” and “perfect,” that trade-off is a daily reality for Tech Leads and Principal Engineers. Much of this is learned on the job — theory only goes so far — but having solid strategies for difficult conversations makes those on-the-fly decisions far easier.&lt;/p&gt;

&lt;p&gt;We discussed real-life scenarios where a decision had to be made and which communication strategies fit best. Questions we explored included: should you be the first to speak, or let others voice their views? When is it useful to wait and let silence work for you? A ten-second pause often elicits valuable input. The more tools you have for framing decisions and guiding discussion, the better your outcomes will be.&lt;/p&gt;

&lt;p&gt;Self evaluation and next steps&lt;br&gt;
As a closing act we ran a self evaluation from start to finish of the process and what we learned from it. With this in mind and her interest, we decided to follow these steps&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creation of a high-level architecture of the application she’s working on and share it with the team. Baby steps first!&lt;/li&gt;
&lt;li&gt;She’s becoming a mentor now, she will use the same structure, in order to expand her level of influence and communication skills.&lt;/li&gt;
&lt;li&gt;Given that she’s lacking in specific technical areas, she will dig deeper into certain topics and the knowledge will be shared with her team and also her area.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mentoring relationship is not over, and I think it is far from over, but I wanted to give you a summary of what has happened so far. I have learned a lot and I realised how many things sounded great in theory but were harder in practice. Now I have run my own self reflection (as mentioned above for her) and this has been so fruitful!&lt;/p&gt;

&lt;p&gt;I was really happy when I received positive feedback from my mentee — it meant the world to me, because something I was not sure I could do definitely paid off. So, if you are hesitating about mentoring somebody, I always recommend you go for it! Even in the worst case, mentoring produces learning for both parties. I speak from experience: some of my past mentorships failed, yet those failures taught me crucial lessons I still use today.&lt;/p&gt;

&lt;p&gt;Special thanks to &lt;a href="https://www.linkedin.com/in/philipp-%E2%98%81%EF%B8%8F-mayer-58a956199/" rel="noopener noreferrer"&gt;Philipp Mayer&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/busayomi/" rel="noopener noreferrer"&gt;Busayo Oyewole&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/stefan-wilke-a4ba73210/" rel="noopener noreferrer"&gt;Stefan Wilke&lt;/a&gt; for helping me review this document.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Resources I have used along the road and that might be useful for you&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zhuo, J. (2019). The Making of a Manager.&lt;/li&gt;
&lt;li&gt;Larson, W. (2021). Staff Engineer.&lt;/li&gt;
&lt;li&gt;Meyer, E. (2014). The Culture Map.&lt;/li&gt;
&lt;li&gt;LinkedIn. (n.d.). &lt;a href="https://www.linkedin.com/advice/3/youre-engineering-manager-who-needs-build-bhruf" rel="noopener noreferrer"&gt;Building Confidence as an Engineering Manager&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Workfeed.ai. (n.d.). &lt;a href="https://workfeed.ai/articles/project-management/cross-cultural-project-management/how-to-build-trust-in-cross-cultural-teams" rel="noopener noreferrer"&gt;Building Trust in Cross-Cultural Teams&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Gambill, T. (2022, July 26). &lt;a href="https://www.forbes.com/sites/tonygambill/2022/07/26/5-characteristics-of-high-trust-teams/" rel="noopener noreferrer"&gt;5 Characteristics Of High-Trust Teams&lt;/a&gt;. Forbes.&lt;/li&gt;
&lt;li&gt;LeadDev. (n.d.). &lt;a href="https://leaddev.com/trust-psychological-safety/three-strategies-building-trust-your-engineering-teams" rel="noopener noreferrer"&gt;Three strategies for building trust with your engineering teams&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;emdiary.substack.com. (n.d.). &lt;a href="https://emdiary.substack.com/p/how-to-stay-motivated-when-nothing" rel="noopener noreferrer"&gt;Staying Motivated When Things Go Wrong&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;LeadDev. (n.d.). &lt;a href="https://leaddev.com/conflict-resolution/managing-conflict-engineering-teams" rel="noopener noreferrer"&gt;Managing conflict in engineering teams&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Wen, J. (n.d.). &lt;a href="https://jamiewen00.medium.com/tech-lead-handbook-manage-conflicts-27688f2905d4" rel="noopener noreferrer"&gt;Tech Lead Handbook — Manage Conflicts&lt;/a&gt;. Medium.&lt;/li&gt;
&lt;li&gt;leadership.garden. (n.d.). &lt;a href="https://leadership.garden/interpersonal-conflicts/" rel="noopener noreferrer"&gt;The No-BS Guide to Engineering Team Conflicts&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Engineering Ladders. (n.d.). &lt;a href="https://www.engineeringladders.com/" rel="noopener noreferrer"&gt;Engineering Ladders&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Highland Literacy. (n.d.). &lt;a href="https://highlandliteracy.com/de-bonos-six-hats/" rel="noopener noreferrer"&gt;De Bono’s Six Hats&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>mentoring</category>
      <category>leadership</category>
      <category>career</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Trust &amp; Transparency: Why we updated our review system at mobile.de</title>
      <dc:creator>Kleinanzeigen &amp; mobile.de</dc:creator>
      <pubDate>Fri, 05 Sep 2025 09:09:29 +0000</pubDate>
      <link>https://dev.to/berlin-tech-blog/trust-transparency-why-we-updated-our-review-system-at-mobilede-70m</link>
      <guid>https://dev.to/berlin-tech-blog/trust-transparency-why-we-updated-our-review-system-at-mobilede-70m</guid>
      <description>&lt;p&gt;an article by &lt;a href="https://www.linkedin.com/in/busayomi/" rel="noopener noreferrer"&gt;Busayo Oyewole&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’ve ever found yourself scrolling through reviews, you know the feeling. One listing has a sparkling 5.0 star rating, but with only three reviews. The other has a slightly lower 4.6, but with thousands of ratings. Which one would you choose?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flava3dloyo67ypy4tzjy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flava3dloyo67ypy4tzjy.webp" alt="(meme from instagram.com)" width="800" height="813"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;center&gt;&lt;em&gt;(meme from instagram.com)&lt;/em&gt;&lt;/center&gt;
&lt;br&gt;

&lt;p&gt;If you’re anything like the person in the meme above, you’d probably pick the 4.6. Why? Because a perfect score with no history feels hollow. It lacks the social proof and depth that only a high volume of feedback can provide. It’s a UX problem we’ve been wanting to solve.&lt;/p&gt;

&lt;p&gt;As the Trust &amp;amp; Safety team, our job is to anticipate these moments of hesitation and build a system that feels genuinely trustworthy. We realised our previous approach (i.e. only showing the total number of reviews from the past two years) was creating this exact paradox. Dealers, who had built a long history of excellent service, felt like their past accomplishments were being completely ignored, not giving them the corresponding credit for their relentless effort. And our users, the buyers, were missing the full story. They were making decisions with only a fraction of the available information, which undermined their confidence.&lt;/p&gt;

&lt;p&gt;We knew we had to do better. So we made a simple but critical change.&lt;/p&gt;

&lt;p&gt;We now display the &lt;strong&gt;total number of reviews&lt;/strong&gt; a dealer has ever received, right next to their star rating. This one metric provides an immediate, powerful signal of a dealer’s credibility and experience. It gives dealers the credit they’ve earned over their entire history and gives buyers the complete picture they need to feel confident.&lt;/p&gt;

&lt;p&gt;Of course, recency still matters. That’s why we still calculate the star rating based on reviews from the last two years. This dual approach gives users the best of both worlds: a comprehensive view of a dealer’s long-term reputation and a look at their recent performance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff3uy9w4uo6fm3xukmkzf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff3uy9w4uo6fm3xukmkzf.webp" alt="(Screenshot sample of about the dealer section from mobile.de)" width="800" height="570"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;center&gt;&lt;em&gt;(Screenshot sample of about the dealer section from mobile.de)&lt;/em&gt;&lt;/center&gt;
&lt;br&gt;

&lt;p&gt;Since we launched this feature, the feedback from our dealers has been incredible. Many have noted that they are now receiving a significant increase in reviews and feel a renewed sense of pride in their complete history.&lt;/p&gt;

&lt;p&gt;For us, the Trust &amp;amp; Safety team, this project is a reminder that user trust isn’t built on a single score; it’s built on a foundation of transparency, context, and a complete picture. It’s about empowering our users to make confident decisions, one review at a time.&lt;/p&gt;




&lt;p&gt;Special thanks to &lt;a href="https://de.linkedin.com/in/gmaldonadol" rel="noopener noreferrer"&gt;Gonzalo&lt;/a&gt;, &lt;a href="https://de.linkedin.com/in/bishalkurumbang" rel="noopener noreferrer"&gt;Bishal&lt;/a&gt; &amp;amp; &lt;a href="https://de.linkedin.com/in/anaromerop/en" rel="noopener noreferrer"&gt;Ana&lt;/a&gt; for reviewing the first draft of this article.&lt;/p&gt;




</description>
      <category>safety</category>
      <category>product</category>
      <category>design</category>
      <category>reviews</category>
    </item>
    <item>
      <title>Rebranding on Android Apps — Behind the Scenes</title>
      <dc:creator>Kleinanzeigen &amp; mobile.de</dc:creator>
      <pubDate>Fri, 15 Aug 2025 08:24:35 +0000</pubDate>
      <link>https://dev.to/berlin-tech-blog/rebranding-on-android-apps-behind-the-scenes-l2l</link>
      <guid>https://dev.to/berlin-tech-blog/rebranding-on-android-apps-behind-the-scenes-l2l</guid>
      <description>&lt;p&gt;an article by &lt;a href="https://www.linkedin.com/in/hannaholukoye/" rel="noopener noreferrer"&gt;Hannah Olukoye&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rebranding an app is no small feat, especially when it involves overhauling the design, implementing a dark theme, and addressing years of technical debt. To understand the challenges, strategies, and lessons learned during our recent rebranding phase, I sat down with one of our Android architects, who shared the team’s experience navigating this transformative project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How did the rebranding affect the existing theming architecture, and did it require any major refactoring?&lt;/strong&gt;&lt;br&gt;
The rebranding wouldn’t have been possible without prior refactoring. Before this, there was no clear structure or guidance on styling components, which also prevented us from enabling dark mode in the Android app. We had too many redundant styles, along with legacy and custom UI components that were inconsistent.&lt;/p&gt;

&lt;p&gt;One of the first steps was to dissolve these legacy components and adopt Material Components. This allowed us to consolidate patterns, such as applying consistent styles to contact form inputs. We also established a proper definition of styles and themes in the app, creating distinct files for specific components and aligning our terminology with the Design System. This made it easier for developers to translate Figma designs into Android layouts.&lt;/p&gt;

&lt;p&gt;Each component was tackled individually — new styles were applied, and old, unused ones were removed. I also communicated with the team about which styles to use and how to implement them. Sometimes, this required layout changes to make the new styles work, which turned into a bigger effort than I initially expected. Interestingly, I ended up reducing technical debt from 8 years ago in the process. (Impressive!)&lt;/p&gt;

&lt;p&gt;The rebranding itself was the final step and took only two weeks to implement once the base refactoring was done. While some adjustments and corrections were necessary, the effort was minimal compared to the groundwork we laid beforehand.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Q: What architectural decisions guided your theme implementation strategy, especially regarding modularization, reuse, and support for feature-based theming?&lt;/strong&gt;&lt;br&gt;
From a modularization perspective, not much changed. We already had our Android resources centralized in a single module accessible to all feature modules. While text and icon assets are often feature-specific, we decided to keep everything in one place.&lt;/p&gt;

&lt;p&gt;A key decision was adopting a clear naming scheme to distinguish components, types, and themes while differentiating our style definitions from those inherited from the Material Components library. This naming scheme followed the Design System conventions, making it easier to maintain consistency.&lt;/p&gt;

&lt;p&gt;By default, we apply base definitions to components and set specific attributes directly rather than creating additional styles, which had previously cluttered our style definitions. When custom changes are necessary, they’re extended within the feature module while adhering to the same naming scheme.&lt;/p&gt;

&lt;p&gt;Consistency was key. Hardcoded values, especially colours, were avoided to ensure support for dark mode and dynamic themes. Developers used token-based definitions tied to system settings. I also reduced icon assets by consolidating them and applying consistent tinting, replacing previously duplicated, hardcoded versions. Special design requests were evaluated collaboratively to see if they could align with the Design System. Simplicity remained our core principle.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Q: How did you test theme behaviour across different devices, screen sizes, and Android versions? Were there any tricky bugs or edge cases?&lt;/strong&gt;&lt;br&gt;
For development, I primarily tested on one emulator with standard settings. Since we use Material Components, I assumed changes would work similarly across older Android versions for individual components. For larger changes, I tested on older versions and tablets as well. I also went through layouts multiple times, testing repeatedly since components were tackled one by one.&lt;/p&gt;

&lt;p&gt;UI tests helped ensure functionality wasn’t broken, and I relied on the Android chapter to test changes in their respective feature areas. Before releases, I used BrowserStack to test on a variety of devices.&lt;/p&gt;

&lt;p&gt;That said, there were challenges. Some widgets, like content cards, weren’t fully updated or cleaned up, which caused issues when testing dark mode. Problems with background colours and missing tint colours were common. Dialogs were particularly tricky — there were many types, each with different implementations and styles, which required significant effort to fix.&lt;/p&gt;

&lt;p&gt;Custom implementations, such as spans or UI inflated in code instead of XML, were another pain point. Finding these usages and ensuring proper styling often required adding extra code. Despite these challenges, properly migrated components worked fine.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Q: What key lessons or best practices did you learn from this redesign, and what would you do differently if starting from scratch?&lt;/strong&gt;&lt;br&gt;
One of the biggest lessons is the importance of early involvement with UX. Understanding what’s coming and getting an initial sense of task complexity helps immensely. Close collaboration with UX throughout the project is also invaluable for resolving unexpected issues or missing assets.&lt;/p&gt;

&lt;p&gt;Another key takeaway is to stick to defaults. Use what the platform and design library offer rather than working against them. This not only ensures backward compatibility but also improves accessibility.&lt;/p&gt;

&lt;p&gt;Regularly updating to the latest versions of libraries is crucial. It fixes bugs and introduces new features that can enhance the app. Additionally, having a clear set of defined styles applied consistently across the app is a must. When changes are needed, they should be made in the base style, so all components are updated simultaneously.&lt;/p&gt;

&lt;p&gt;If I were starting from scratch, I’d likely use Compose from the beginning. While it’s just a different approach to styling, the same principles around themes, styles, and components would still apply. Compose offers a more modern and flexible way to build UI, which could streamline the process further.&lt;/p&gt;




&lt;p&gt;This rebranding effort — along with the introduction of dark theme — gave the app a fresh, modern look while laying the groundwork for a more maintainable and scalable design system. The lessons learned and best practices established during this phase will continue to influence and elevate Android development at our company.&lt;/p&gt;

&lt;p&gt;You can experience the new design by downloading the mobile.de app on &lt;a href="https://play.google.com/store/apps/details?id=de.mobile.android.app&amp;amp;referrer=utm_source%3Dwww.mobile.de%26utm_medium%3Ddownload_button%26utm_campaign%3Dfooter_link" rel="noopener noreferrer"&gt;Android&lt;/a&gt; and &lt;a href="https://apps.apple.com/us/app/mobile-de-car-market/id378563358?pt=375847&amp;amp;ct=www.mobile.de-download_button-footer_link&amp;amp;mt=8" rel="noopener noreferrer"&gt;iOS&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Many thanks to &lt;a href="https://www.linkedin.com/in/thomas-rebouillon-60234186/" rel="noopener noreferrer"&gt;Thomas Rebouillon&lt;/a&gt; for sharing his insights and contributions throughout the rebranding process.&lt;/p&gt;

</description>
      <category>android</category>
      <category>designsystem</category>
      <category>mobile</category>
      <category>resources</category>
    </item>
    <item>
      <title>Handling User Migration with Debezium, Apache Kafka, and a Synchronization Algorithm with Cycle Detection</title>
      <dc:creator>MD Sayem Ahmed</dc:creator>
      <pubDate>Thu, 12 Jun 2025 09:39:17 +0000</pubDate>
      <link>https://dev.to/berlin-tech-blog/handling-user-migration-with-debezium-apache-kafka-and-a-synchronization-algorithm-with-cycle-mg9</link>
      <guid>https://dev.to/berlin-tech-blog/handling-user-migration-with-debezium-apache-kafka-and-a-synchronization-algorithm-with-cycle-mg9</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Migrating millions of users' data without downtime or loss is a monumental challenge. At Kleinanzeigen, we tackled this problem recently when we migrated our users' data from a legacy platform to a new one with the help of Change Data Capture (Debezium), Apache Kafka, and a custom synchronization algorithm with built-in cycle detection, and thus, ended up creating a data synchronization system that mimics many of the key properties of a Distributed Database. This blog post will describe the business case that started the migration, our thought process for defining the architecture and technology choices, the trade-offs we made when agreeing on a solution, and the final architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.kleinanzeigen.de/" rel="noopener noreferrer"&gt;Kleinanzeigen&lt;/a&gt; (KA for short) is a leading classifieds platform in Germany. It is also the number one platform in &lt;a href="https://en.wikipedia.org/wiki/Recommerce" rel="noopener noreferrer"&gt;re-commerce&lt;/a&gt;, with 33.6 million unique visitors per month and 714 million visits in total. Suffice to say, it is a powerhouse in Germany.&lt;/p&gt;

&lt;p&gt;KA recently migrated the whole platform to a new system. For this purpose, we created the new system, ran it parallel to our legacy system, migrated all users’ data to the new system, and then incrementally switched users to the new one. We kept both platforms operational simultaneously to make incremental switching possible, which helped us avoid a high-risk big-bang migration. Also, if something goes wrong with the new platform, we could always revert the users to the old system.&lt;/p&gt;

&lt;p&gt;In addition to migrating the data, we also implemented significant user data transformations. The transformations were necessary because KA has been an extremely successful company, so naturally, we had accrued technical debts over the years. As part of this migration, we wanted to eliminate at least some of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Figuring Out How to Orchestrate the Migration
&lt;/h2&gt;

&lt;p&gt;We first had to figure out how to orchestrate the whole migration process. A user at KA has profile data, ads, transactions, messages, etc. The migration process is complicated by the strict dependencies among all the data. Suppose we tried migrating a user’s ads before their profile data had been migrated? The ad migration would fail, because it cannot be attached to a user. It was important to carefully coordinate the migration sequence to ensure all data dependencies were handled.&lt;/p&gt;

&lt;p&gt;After numerous planning sessions, workshops, and collaborative discussions involving multiple teams, we decided on the following migration strategy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We would first migrate the core data of a user: their unique user IDs, their authentication information, and their profile data.&lt;/li&gt;
&lt;li&gt;Once we had migrated core user data, the new system would start to recognize the user IDs, which were needed before any other data (e.g., ads, transactions) could be migrated.&lt;/li&gt;
&lt;li&gt;After core user data migration, we would inform all the dependent systems to start subsequent data migrations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibuwl05ee8e6t5nkkvy4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibuwl05ee8e6t5nkkvy4.webp" alt="Figure 1: User data migration sequence" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our team — Team Red at KA — was responsible for migrating the core user data and bootstrapping the entire migration process, which we will focus on in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  First High-Level Architecture
&lt;/h2&gt;

&lt;p&gt;We initially developed the following architecture to migrate core user data -&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frjg1gksvn50dplr2ktr6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frjg1gksvn50dplr2ktr6.webp" alt="Figure 2: First high level architecture" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We used a reverse proxy to intercept all incoming user requests, with just enough business logic to determine where to forward a user’s traffic. Existing users would continue to interact with our legacy system. However, once a user’s data had been migrated and the user had been switched to the new platform, the proxy would forward the user’s traffic to the new system.&lt;/p&gt;

&lt;p&gt;We also decided to use an asynchronous streaming architecture rather than attempting to dual-write both systems synchronously from a single place/service. The reasons for this decision are below:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Even though the dual-write might seem simpler at first because we won’t need any additional system between the legacy and the new system, and it would also appear to enforce strong consistency, our experience showed that trying to synchronize multiple systems with synchronous calls is operationally challenging. What happens when the write to the legacy system succeeds but the write to the new system fails? Or vice versa? As we all know, &lt;a href="https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing" rel="noopener noreferrer"&gt;the network is the most unreliable part of any distributed architecture&lt;/a&gt;, and failure due to a network issue is very likely because synchronous calls introduce temporal coupling between the client and the server. In such cases, we would need to introduce additional infrastructure (i.e., &lt;a href="https://microservices.io/patterns/data/saga.html" rel="noopener noreferrer"&gt;implement Saga&lt;/a&gt;) to handle such failures, and ultimately, our system would become eventually-consistent.&lt;/li&gt;
&lt;li&gt;Choosing between the CAP theorem’s C (consistency) and A (availability) is always a business decision, and our business was fine with a slight syncing delay of up to a minute between the two systems.&lt;/li&gt;
&lt;li&gt;As discussed before, we also needed a place to implement the data transformation logic to address technical debts. The dual-write approach would have required us to either modify the legacy system, which was a complex undertaking, or write the transformation in the new system, which was being developed by a whole separate team. As a result, we did not have much control over it (&lt;a href="https://en.wikipedia.org/wiki/Conway%27s_law" rel="noopener noreferrer"&gt;Conway’s Law&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Backsyncing Users’ Data from New to Legacy System
&lt;/h2&gt;

&lt;p&gt;The high-level diagram simplified many elements. For example, the legacy system was not a simple “box”, as the picture showed. The box is an abstraction of numerous services with complex interconnections between them. Both the legacy system and the new system were complex. &lt;a href="https://how.complexsystems.fail/" rel="noopener noreferrer"&gt;Since complex systems inherently possess multifaceted failure modes&lt;/a&gt;, we wanted to ensure that in case of any failures in the new system, we could switch all users back to the legacy system. This approach would require us to propagate all migrated users’ updates from the new system to the legacy system.&lt;/p&gt;

&lt;p&gt;Also, as mentioned before, we wanted to incrementally switch our users to the new platform to avoid a big-bang migration (we called these users transitioned users). After both systems were prepped and ready, we planned to transition a few test users. Following that, we would transition a tiny percentage of users to beta-test the platform before steadily ramping up. While these users were using the new system — creating and updating their data (i.e., creating ads) — we also needed to send these updates to the legacy system because that’s where most users would remain. If we did not send these changes there, the transitioned users’ ads would not receive much visibility.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe2p7vwaszo74dqm15flx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe2p7vwaszo74dqm15flx.webp" alt="Figure 3: Need for backsyncing users’ data" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With those decisions in mind, we focused on our next challenge: How could we capture users’ updates in the legacy system and stream them to the new system?&lt;/p&gt;

&lt;h2&gt;
  
  
  Capturing User Updates with Transactional Outbox
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://microservices.io/patterns/data/transactional-outbox.html" rel="noopener noreferrer"&gt;Transactional Outbox&lt;/a&gt; is a commonly used pattern for capturing updates to an entity and sending them to a remote system. We considered using it to capture changes to the user entity -&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9d7xuz4ucd8y2wieujai.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9d7xuz4ucd8y2wieujai.webp" alt="Figure 4: Architecture implementing transactional outbox to capture user changes" width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As the above diagram shows, we considered creating a new table — UserChanged — to store the change events required for an outbox implementation. An event publisher would poll this event table and send the events to a Kafka topic, which would then be read by the mapping service and sent to the new system. However, we realized that this approach won’t scale. KA has millions of users, and their core data is updated millions of times per day. Thus, a polling job would not be able to keep up with the update rate, especially if we wanted to ensure speedy delivery of updates to the new system.&lt;/p&gt;

&lt;p&gt;We then considered leveraging &lt;a href="https://docs.spring.io/spring-framework/reference/data-access/transaction/event.html" rel="noopener noreferrer"&gt;Spring’s Transactional Event Listeners&lt;/a&gt; to publish the events in real-time to Kafka. However, we quickly realized that there are some edge cases where we might send a user’s updates in the wrong order, leading to inconsistencies between the two systems.&lt;/p&gt;

&lt;p&gt;Another problem with the outbox implementation was that every team needed to do the same repetitive work — create change events for entities like ads and transactions, store them in relevant tables, and then publish them to Kafka. It would be great to eliminate some of the duplicate work here.&lt;/p&gt;

&lt;p&gt;Finally, and most importantly, we would have to scan through our legacy system to implement the outbox pattern and identify where we modified the entities. Referring to our earlier comment that KA has been a successful company which grew at a fantastic speed and thus accumulated technical debts, we could not rule out the possibility that we might forget a few places where database updates were taking place and, therefore, miss capturing those changes. If we did not send those changes to the new system, we would create inconsistencies that would be hard to detect and fix. At this point, we asked ourselves if there was a better way. Can we intercept the changes from the database directly?&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Change Data Capture with Debezium
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://debezium.io/" rel="noopener noreferrer"&gt;Debezium&lt;/a&gt; is an open-source distributed platform capable of streaming changes directly from a database. It hooks into a database like MySQL’s transactional log, captures every change performed on the database, and then publishes them to a Kafka topic. For the use case that we had for this migration, it sounded like an excellent fit. However, our team was also aware that a &lt;a href="https://boringtechnology.club/#25" rel="noopener noreferrer"&gt;new technology introduces a lot of unknown failure modes&lt;/a&gt;. Even though Debezium was already a few years old by then, KA had never used it before, and we did not have anyone with prior experience setting it up and using it.&lt;/p&gt;

&lt;p&gt;After thinking about it for a while and determining that we had a &lt;a href="https://boringtechnology.club/#17" rel="noopener noreferrer"&gt;few innovation tokens&lt;/a&gt; for our team that we could afford to spend researching a new technology that might also help other teams, we decided to try Debezium.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges with Debezium
&lt;/h2&gt;

&lt;p&gt;Our first job was to figure out how to set it up within the existing KA infrastructure. At that time, KA used a MySQL cluster with a single leader and multiple follower instances. The cluster was set up across two data centres (DC for short), with stand-by leaders on each DC. Only one of these leaders would be acting as the current leader at any given time, while the others would act as followers. Unfortunately, we discovered several issues with our cluster setup, as described below.&lt;/p&gt;

&lt;p&gt;The first issue that we encountered was due to the transaction log format. MySQL’s transaction logs (also known as Binlog) store all changes applied to the database in &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/binary-log-formats.html" rel="noopener noreferrer"&gt;one of the three formats&lt;/a&gt; -&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Statement-based log&lt;/li&gt;
&lt;li&gt;Row-based log&lt;/li&gt;
&lt;li&gt;Mixed-mode log&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In a statement-based log, MySQL stores the actual SQL statements executed on the server in the log. Then, during replication, the leader sends these statements to the followers so they can execute the same statements and thus get the same set of changes. However, this type of log format is no longer recommended as, for certain types of queries, it can lead to non-deterministic outcomes and, as a result, create inconsistencies between a leader and a follower.&lt;/p&gt;

&lt;p&gt;In a row-based format, MySQL stores the actual change applied to the database when an SQL statement is executed. This format is recommended nowadays as it does not have non-determinism issues like the statement-based format.&lt;/p&gt;

&lt;p&gt;The third form, mixed mode, is a format where the Binlog supports statement- and row-based formats.&lt;/p&gt;

&lt;p&gt;For Debezium to retrieve all changes from a MySQL database, the Binlog must be in row-based format. However, all instances in our MySQL cluster at KA used statement-based formats. We learned that it would take considerable time and effort to change the format from statement- to row-based for all instances, which our user migration process could not afford then. In addition, it would also require a substantial amount of effort from our site reliability engineers, which was also difficult to arrange.&lt;/p&gt;

&lt;p&gt;Another challenge we ran into was with the cluster itself. Debezium documentation recommended enabling &lt;a href="https://dev.mysql.com/doc/mysql-replication-excerpt/5.7/en/replication-gtids-concepts.html" rel="noopener noreferrer"&gt;Global Transaction Identifier&lt;/a&gt; (GTID for short) for a single-leader cluster setup like ours. Enabling GTID ensures that in the event of a leader failure, when a follower gets promoted to be the new leader, Debezium can continue reading the Binlog. In such a case, Debezium uses a &lt;a href="https://debezium.io/documentation/reference/3.2/connectors/mysql.html#mysql-property-gtid-source-includes" rel="noopener noreferrer"&gt;GTID sequence check&lt;/a&gt; to position itself correctly in the new leader’s Binlog. Unfortunately, our cluster was not GTID-enabled.&lt;/p&gt;

&lt;p&gt;To resolve these challenges, we came up with some pragmatic solutions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We provisioned two new MySQL instances, one in each DC, with their Binlog format set to row. These instances would act like any other followers, and like any other follower, they would get their changes from the leader in a statement-based format, except that their own Binlog format would be row-based. Debezium would follow one of these instances to read the database changes.&lt;/li&gt;
&lt;li&gt;We added a reverse proxy that would route traffic to one of these two row-based MySQL instances at a time while the other one was on standby. We then created a Debezium connector config using the proxy as a database host, effectively abstracting the two-instance small-cluster setup.&lt;/li&gt;
&lt;li&gt;We decided that in case of a failure in the database the proxy was pointing to, we would switch traffic to another instance. At this point, the Debezium connector would fail to start, and a simple restart of the connector would not be enough for it to resume working. To resume the connector, we would drop the internal config topic where Debezium stores the connector config information, recreate the topic again, and restart the Debezium connector. At that point, the connector would start working again. The alternative would have been to allow Debezium to perform &lt;a href="https://debezium.io/documentation/reference/3.2/connectors/mysql.html#mysql-property-snapshot-mode" rel="noopener noreferrer"&gt;a full snapshot of the database&lt;/a&gt;, which we did not want to do because that would mean sending millions of updates to downstream systems in an uncontrolled manner, which could impact system stability.&lt;/li&gt;
&lt;li&gt;Debezium would miss the updates in our database during the failure and the switching. To capture those changes, we relied on the &lt;a href="https://debezium.io/documentation/reference/3.2/connectors/mysql.html#mysql-snapshots" rel="noopener noreferrer"&gt;snapshotting capability&lt;/a&gt; — we would issue snapshot commands for each table, which would replay all changes that had happened since a specific time in the past.&lt;/li&gt;
&lt;li&gt;These decisions allowed us to run Debezium without making it cluster-aware, thus avoiding the necessity of changing all instances’ log format and having GTID.&lt;/li&gt;
&lt;li&gt;We also increased MySQL’s Binlog retention period to at least seven days so that Debezium would not miss any changes while the connector was not running.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We tested every decision we discussed above locally by simulating our production infrastructure. We created the MySQL cluster, set up Kafka topics, and then configured Debezium to read from the instances — all using docker-compose and a local producer/consumer application. We also tested the failover scenarios and the process of switching Debezium to follow a different instance and resume it after re-creating config topics. We tested other failure scenarios as well. All of these were done to help us discover &lt;a href="https://boringtechnology.club/#28" rel="noopener noreferrer"&gt;as many unknowns as possible&lt;/a&gt;: we wanted to eliminate as many failures as we could from the final architecture (or at least monitor them with correct metrics and alerts and update our operational runbooks with appropriate actions to take).&lt;/p&gt;

&lt;p&gt;After testing everything and gaining more confidence, we installed Debezium in our infrastructure with support from our site reliability engineers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8nibc03tivenird6epq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8nibc03tivenird6epq.webp" alt="Figure 5: Architecture after adding Debezium" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracking Migration Phases
&lt;/h2&gt;

&lt;p&gt;After we figured out how to capture user changes, our next focus was tracking migration status for each user. Based on the requirements, we needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;To track the number of users ready to be migrated, either because their data has been transformed or they do not need any transformation at all.&lt;/li&gt;
&lt;li&gt;To track how many users’ data have been migrated at any point in time.&lt;/li&gt;
&lt;li&gt;To figure out a way to selectively transition our first small batch of test users to the new platform so that they can test it.&lt;/li&gt;
&lt;li&gt;A way for other sub-systems to get notified whenever a user’s data migration has been completed, so that they can also trigger the migration of their relevant data.&lt;/li&gt;
&lt;li&gt;A way to expose this phase information to our reverse proxy, as it decides whether the new system or the legacy system receives a user’s traffic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When we examined these requirements closely, we realized we needed to implement a state machine to track different phases of the migration process. But we still needed to figure out the exact states and where we would store them.&lt;/p&gt;

&lt;p&gt;We also realized that user migration is similar to how a person moves from one country to another. For example, when one of the Team Red members wanted to move to Germany, they went through the following phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They decided that they would like to work in Germany.&lt;/li&gt;
&lt;li&gt;They applied for a job and got a job offer.&lt;/li&gt;
&lt;li&gt;Once getting the job offer, they applied for a work visa at the German Embassy.&lt;/li&gt;
&lt;li&gt;Once getting the visa approval, they moved to Germany.&lt;/li&gt;
&lt;li&gt;After living in Germany for a few years, they decided to stay for a long time and got a Permanent Settlement Permit, which allowed them to live in Germany indefinitely.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Applying this analogy to our user migration process, we came up with the following migration states -&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;IDENTIFIED: A user that needs to be migrated. They may not be migrated immediately because their data might require some transformation, or they may not be one of the users selected for migration. But we know they will eventually migrate to the new system.&lt;/li&gt;
&lt;li&gt;ELIGIBLE: The user has fulfilled all the requirements for migration — their data has been transformed, and/or they have been chosen for the migration.&lt;/li&gt;
&lt;li&gt;MIGRATION_REQUESTED: A migration request has been issued to the new system, which is now setting up the account. The user account may take a while to be created in the new system. From this point on, any changes to the user’s core data in the legacy system will be propagated to the new system.&lt;/li&gt;
&lt;li&gt;MIGRATED: The new system has confirmed that the user account has been created.&lt;/li&gt;
&lt;li&gt;TRANSITIONED: The user has transitioned to the new system, and all user updates are now happening on the new system.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The following picture displays the relationship between the user migration states and the migration phases of a person -&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fru7mlml3xxci0kol5e6w.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fru7mlml3xxci0kol5e6w.webp" alt="Figure 6: Relationship between real-life immigration phases and our user migration states" width="800" height="615"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We then started thinking — where do we put this state information? Storing them with the user data would not make sense, as they were not related and were only needed during the migration. Also, once the migration is over, we will remove them once and for all. We applied a similar migration analogy to find a solution and developed a new type - Emigrant. An emigrant is a person who has left their home country and moved to another. Since KA users are also leaving their old platform for a new one, they are all emigrants from the legacy platform’s perspective. We then created an emigrant entity in our legacy system and stored this state there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2sgwmk9ww3a4whaj7vzm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2sgwmk9ww3a4whaj7vzm.webp" alt="Figure 7: Emigrant state machine" width="800" height="641"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We then mapped our entire migration process as transitions in this emigrant state machine. The following diagram shows all possible transitions -&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8hdxxvtiavqjmzjw0kf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8hdxxvtiavqjmzjw0kf.webp" alt="Figure 8: Possible state transitions in emigrant state machine" width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We also decided to use Debezium to capture all changes happening to an emigrant. This decision helped us organize the migration process as a series of small and consistent updates, as we will see shortly.&lt;/p&gt;

&lt;p&gt;This is what the architecture looked like after plugging in the emigrant concept -&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8nf21c1nfohubdhjfi14.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8nf21c1nfohubdhjfi14.webp" alt="Figure 9: Architecture after plugging in emigrant state machine" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating Emigrants
&lt;/h2&gt;

&lt;p&gt;The following diagram shows the logic that we used to create emigrants -&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flxsme7m57t08dublmq3p.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flxsme7m57t08dublmq3p.webp" alt="Figure 10: Emigrant creation process" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As the diagram shows, Debezium sends all user updates to the syncing service via Kafka. The Kafka listener then checked if an emigrant existed in the legacy database and, if not, created one. As discussed earlier, the emigrant’s initial state would be IDENTIFIED.&lt;/p&gt;

&lt;p&gt;We had a data transformation logic check within the same service to verify if an emigrant’s data needed to be transformed before starting their migration. Once verified, that check changed the emigrant’s state from IDENTIFIED to ELIGIBLE, and the change was committed to the database. Debezium would then pick up the change and publish it to the respective topic (emigrant). The listener, which was implemented within the same syncing service, would pick up this state change. Since Debezium CDC payloads contain two fields that include the state of the database record before the change and the state of the record after applying the change, the listener would then be able to determine that a state transition had taken place where the old state was IDENTIFIED and the new state was ELIGIBLE (in fact, it checked if the previous state was anything other than ELIGIBLE, which allowed us to reuse this flow for another case, as we will see in a bit). It would then treat it as a signal to start the migration and send an HTTP request to the new system. If the request was successful, it would change the emigrant state to MIGRATION_REQUESTED, and in case it failed, the migration state would be changed to MIGRATION_FAILED. It would then commit the change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fae86lkmk9nhv0n5zfhem.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fae86lkmk9nhv0n5zfhem.webp" alt="Figure 11: Starting an emigrant’s migration" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To retry failed migrations, we had a job to replay them by simply changing the state back to ELIGIBLE, and the above flow would start executing again. Since &lt;a href="https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing" rel="noopener noreferrer"&gt;network failures are common&lt;/a&gt; when making HTTP requests, this design allowed us to handle transient network failures easily. We also ensured the account creation process was idempotent to avoid duplicate accounts. Also, when an emigrant moved to the MIGRATION_REQUESTED state, the syncing service started syncing every change to the respective core user data. The data changes would also continue syncing when emigrants moved to the MIGRATED state.&lt;/p&gt;

&lt;p&gt;Once the new system successfully created the account, an HTTP endpoint in the mapping service was called to confirm the migration. The controller in the mapping service would then change the emigrant state to MIGRATED and commit it. This state change would then be picked up by the emigrant listener, which, like before, would detect that a state change had taken place and send a confirmation message to a topic called &lt;code&gt;user-migrated&lt;/code&gt;, thus notifying all downstream systems that an emigrant had just migrated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fufwgmdk8srnudpj0y9fi.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fufwgmdk8srnudpj0y9fi.webp" alt="Figure 12: Marking an emigrant as migrated" width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting Emigrants
&lt;/h2&gt;

&lt;p&gt;The following diagram shows the deletion logic used to delete emigrants -&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fej0ye75mslj2r3kyq7as.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fej0ye75mslj2r3kyq7as.webp" alt="Figure 13: Deleting emigrants" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The deletion process would start whenever we received a user deletion event via Debezium. Even if an emigrant did not exist in the database, we sent a delete request to the new system. This would ensure we never leave a deleted user’s data in the new system, even if some unforeseen inconsistency caused missing emigrants in the database. We took this extra, unnecessary (but not too costly) step to fully comply with GDPR. Since the delete operation on the new system was idempotent, it did not cause any issues.&lt;/p&gt;

&lt;p&gt;However, if an emigrant existed for the user, then after sending the delete request to the new system, the process would delete the emigrant. Then, rather than publishing a tombstone to the user-migrated topic immediately, we relied on Debezium to capture the emigrant’s deletion and then published the deletion confirmation to the user-migrated topic. This allowed us to be more resilient in the face of failures. Instead of ensuring three different syncing operations — deleting emigrants, sending delete to the new system, and publishing tombstone to the user-migrated topic — succeeded one after another when a user was deleted, we needed to focus on only two (deleting emigrants and sending delete to the new system). But most importantly, it is symmetrical to how we publish migration events to this topic (after emigrant is migrated) and conceptually more clearer — we emit tombstones only when the emigrant is deleted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Forward-Sync Architecture
&lt;/h2&gt;

&lt;p&gt;Connecting all the processes described above, this is what the complete forward-sync architecture looks like -&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3cz85vx8gge8mqf29kqs.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3cz85vx8gge8mqf29kqs.webp" alt="Figure 14: Architecture handling full forward sync from the legacy to the new system" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few points about the architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debezium’s ability to capture data before and after a committed transaction was tremendously helpful. It allowed the emigrant listener to accurately determine what changed in a particular transaction. This, in turn, allowed us to (re) start a migration whenever the new state was ELIGIBLE, and the old state was anything but. As a result, we could reuse the same logic when replaying a failed migration.&lt;/li&gt;
&lt;li&gt;Because of Debezium, we could also break down the migration process into smaller atomic chunks that revolved around committed state changes of emigrants. For example, once receiving the account confirmation request, the controller in the mapping service changed the emigrant state to MIGRATED. It did not need to publish a confirmation message to the user-migrated topic at the same time. As a result, we did not need to deal with corner cases such as the state change being successful but the message publication failing. If the state change failed, the controller returned a 5xx to the client, who would retry. If the publication of migration confirmation to the user-migrated topic failed, the emigrant listener would keep retrying until it was successful. We collected metrics to monitor every topic’s lag and triggered alerts if it exceeded a certain threshold. However, for some use cases, we also implemented a dead letter queue using a database table where we would push the failing records after trying to process them a certain number of times.&lt;/li&gt;
&lt;li&gt;In addition to publishing confirmation messages to the user-migrated topic, we also exposed an HTTP endpoint for services that relied on polling/querying to determine an emigrant’s migration state.&lt;/li&gt;
&lt;li&gt;We did not create separate microservices for different operations. Instead, we kept them all within the same service. We used modular monolith practices and created cohesive modules that properly separated concerns within the same service. This decision allowed us to leverage the ACID properties of MySQL transactions to build our synchronization algorithm, as we will see in a bit. Also, we avoided a lot of overhead that typically occurs with a microservice-oriented architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Transitioning Emigrants
&lt;/h2&gt;

&lt;p&gt;Next, it was time to figure out how to transition emigrants to the new platform. A different team was working on a separate service that would decide when to transition an emigrant. Once decided, the transition process would start by sending a command to the mapping service. The following diagram shows the modified architecture after adding these components -&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc6fc5u81s8i85cmwbpdk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc6fc5u81s8i85cmwbpdk.webp" alt="Figure 15: Architecture after adding components required for emigrant transitions" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After receiving the transition request, the mapping service would change the emigrant’s state to TRANSITIONED. From then on, the reverse proxy would forward this user’s traffic to the new platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backsyncing Transitioned Emigrants’ Data
&lt;/h2&gt;

&lt;p&gt;We discussed the need to backsync a transitioned emigrant’s updates with the legacy platform. To do that, we added another HTTP endpoint in our mapping service. When a transitioned emigrant’s data got updated in the new system, it would send the update to the mapping service via that endpoint. The mapping service would then update the emigrant’s data in the legacy database. The following diagram shows the additional components needed for this job -&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc0ki4rl5fawih0oa826w.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc0ki4rl5fawih0oa826w.webp" alt="Figure 16: Architecture after adding components required for backsyncing users’ data" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Allowing Bi-Directional Updates
&lt;/h2&gt;

&lt;p&gt;Until now, the architecture implementation has made one implicit assumption — a user’s data would only be updated on one platform. Before a user transitioned, all their updates occurred on the legacy platform. Once they transitioned, all their updates happened on the new platform. This assumption helped us avoid scenarios like an infinite update loop between the two systems. Let us explain how.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficefwb6boluetjyt3ujn.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficefwb6boluetjyt3ujn.webp" alt="Figure 17: Example demonstrating infinite update loop between the legacy and the new system" width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Suppose our system allows user data updates on both platforms simultaneously, and a transitioned user’s data has just been updated on the legacy platform. This update will arrive in the mapping service via Debezium. Debezium will send this update to the new system. Seeing that this is a transitioned user, the new system will update its copy and re-send the update to the mapping service via HTTP call. Seeing that this is a transitioned user, the mapping service will update it on the database again, which will then be captured via Debezium and sent to the mapping service. Thus, an update loop would have been established.&lt;/p&gt;

&lt;p&gt;One might assume there would be no changes to the user’s data during later updates in the loop described just now; since the mapping service used JPA/Hibernate, the update might not even trigger any actual SQL updates. However, that assumption did not last long, as the mapping service relied on the new service to get a transitioned user’s data modification time. The new system always sets the modification time for transitioned users to the current timestamp. As a result, the modification time would always keep changing, resulting in actual SQL updates.&lt;/p&gt;

&lt;p&gt;We initially considered restricting a user’s update to a single platform at a time to avoid this loop. However, we realized that this restriction was not realistic. A transitioned user’s traffic would only be forwarded to the new system when the reverse proxy in front of our infrastructure identifies the user as a transitioned user. To do that, it would at least need the user ID, and that user ID would only be available if the user had logged in. But what if the user had not logged in, but their data would still have been updated? This could happen when a user resets their password. In such scenario, the password would be updated in the legacy system, which would then need to be synchronized with the new system. Another use case would be when a user account would be deemed fraudulent. For such cases, even though we had an alternative moderation tool available in the new system, we still wanted to keep our old tool available in case of an emergency and/or our customer support agents forgot to check if the user had transitioned. Since providing a safe platform for our users is one of KA’s highest priorities, we wanted to ensure that operations like blocking a fraudulent user account could still be done on the legacy system and then synced with the new system. But to do that, we needed to figure out a way to break the update loop.&lt;/p&gt;

&lt;p&gt;One proposed solution to break the loop was introducing a new field to track which platform an update originated from. The field could be named as “update_source”. When an update takes place on the legacy system, it would have “LEGACY” as the value; for new system updates, it would contain “NEW”. But to use that field, we needed to find all the places where user updates were taking place on both the legacy and the new platform, a challenge that we already saw as too daunting and error-prone.&lt;/p&gt;

&lt;p&gt;Another idea was to use the modification time to determine if an update was stale. Ignoring that the new system always used the latest time as the modification time, and as a result, it was constantly changing, using the physical time to determine if an update was fresh in a distributed system has other issues, too. For example, protocols like Network Time Protocol that keep machine times in sync &lt;a href="https://en.wikipedia.org/wiki/Network_Time_Protocol#Clock_synchronization_algorithm" rel="noopener noreferrer"&gt;always have room for errors&lt;/a&gt;, which could lead to clock skews between two machines in the range of a few milliseconds to seconds. Our migration system handled migrations for millions of users and synced millions of updates daily, so even the slightest deviation could result in thousands of infinite updates going round and round, which would be hard to detect and stop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Logical_clock" rel="noopener noreferrer"&gt;Logical clocks&lt;/a&gt; are a popular alternative to physical clocks in distributed systems. An example of such a clock would be the partition offset assigned to each record in a Kafka topic. Consensus algorithms like &lt;a href="https://en.wikipedia.org/wiki/Raft_(algorithm)" rel="noopener noreferrer"&gt;Raft&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Paxos_(computer_science)" rel="noopener noreferrer"&gt;Paxos&lt;/a&gt; also rely on logical clocks to determine which updates are recent. But where can we find a logical clock in our system?&lt;/p&gt;

&lt;p&gt;While trying to figure out a solution, we noticed an “interesting” field in our user entity. As mentioned, we used JPA/Hibernate as our ORM to handle data persistence. With any JPA entity, it has become a standard practice to include a &lt;a href="https://docs.oracle.com/javaee/7/api/javax/persistence/Version.html" rel="noopener noreferrer"&gt;@Version field&lt;/a&gt;. These fields help JPA implement optimistic locking when updating the entity in the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkhurhl5d0xi5ugisrpdf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkhurhl5d0xi5ugisrpdf.webp" alt="Figure 18: An example user entity in JPA" width="800" height="844"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When numeric types like Long are used as &lt;a class="mentioned-user" href="https://dev.to/version"&gt;@version&lt;/a&gt;, due to the way Hibernate implements optimistic locking, they become -&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Monotonically increasing: they are either incremented and stored in the database, or the update fails and gets rolled back. They never decrease.&lt;/li&gt;
&lt;li&gt;Atomic: they are either incremented and persisted in the database, or the update is rolled back.&lt;/li&gt;
&lt;li&gt;Unique for each successful update of an individual user: when persisted/committed into the database, each update is guaranteed a new version value for each individual user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We realized that these properties make the Version field an ideal logical clock for our migration system. We then used it and developed our synchronization algorithm to determine which updates must be propagated between the two systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Synchronization Algorithm to Break Update Loop
&lt;/h2&gt;

&lt;p&gt;We decided to store the &lt;code&gt;version&lt;/code&gt; value of the user entity with the emigrant entity in a field called &lt;code&gt;user_version&lt;/code&gt;. The mapping service kept updating this value whenever it received a user update via Debezium that had a &lt;code&gt;version&lt;/code&gt; greater than &lt;code&gt;user_version&lt;/code&gt;. However, whenever the service received an update whose &lt;code&gt;version&lt;/code&gt; was less than or equal to &lt;code&gt;user_version&lt;/code&gt;, it identified the update as one it had already seen.&lt;/p&gt;

&lt;p&gt;For transitioned emigrants, whenever their data changed in the new system, the mapping service would start updating it in the legacy system by starting a transaction in MySQL. Once the user data had been updated, it would update the &lt;code&gt;user_version&lt;/code&gt; to the latest &lt;code&gt;version&lt;/code&gt;, in the same transaction. It would then commit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8my8oe2stnbimjzyaap.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8my8oe2stnbimjzyaap.webp" alt="Figure 19: An example demonstrating regular updates for transitioned users" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This change would then arrive at the mapping service via Debezium. Since this update’s &lt;code&gt;version&lt;/code&gt; value would be the same as &lt;code&gt;user_version&lt;/code&gt;, the service would deduce that this was a user update it had already seen before and would ignore the change.&lt;/p&gt;

&lt;p&gt;Let’s see what happens when an update, such as a password reset, originates in the legacy system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jq61kqz4zj3pm84oetb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jq61kqz4zj3pm84oetb.webp" alt="Figure 20: An example demonstrating the use of  raw `version` endraw  to break infinite update loop when update originates in the legacy system" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Suppose that a user had just reset their password. Then -&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Once the password had been reset, the update would arrive via Debezium at the mapping service. Suppose that the &lt;code&gt;version&lt;/code&gt; value in this update is 8.&lt;/li&gt;
&lt;li&gt;Since the user update had just occurred and the mapping service had not seen it before, the &lt;code&gt;user_version&lt;/code&gt; in emigrant must have a value of less than 8 (considering the property of this field/logical clock we discussed before). Let’s assume the value in the &lt;code&gt;user_version&lt;/code&gt; field was 7.&lt;/li&gt;
&lt;li&gt;Since this is a user update the mapping service had not seen before, it would update the &lt;code&gt;user_version&lt;/code&gt; in emigrant to 8 and sync this update with the new system.&lt;/li&gt;
&lt;li&gt;The new system would update its copy, and seeing that this was a transitioned user, it would send the update back to the mapping service via HTTP call.&lt;/li&gt;
&lt;li&gt;The mapping service would then start updating the user data again. It would begin another transaction in MySQL, update the user data, which would cause the &lt;code&gt;version&lt;/code&gt; value in the user entity to increment to 9, store this updated value in the &lt;code&gt;user_version&lt;/code&gt; field in the emigrant entity, and commit the transaction.&lt;/li&gt;
&lt;li&gt;This user update would then again arrive at the mapping service via Debezium. But this time, the mapping service would see that the &lt;code&gt;user_version&lt;/code&gt; in emigrant was already 9. Hence, it would identify it as an already-seen/processed update and ignore it, breaking the loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We tested the flow with a few transitioned users and found that the algorithm worked as expected. We also noticed one interesting fact — sometimes our mapping service would receive user updates where the &lt;code&gt;version&lt;/code&gt; field had a value of, let’s say, 5, while we had 7 stored in &lt;code&gt;user_version&lt;/code&gt; in emigrant. It would then receive the update with version 6 and finally with version 7. We attributed it to possible network slowness that caused Debezium and/or Kafka brokers to deliver updates slowly to one topic while other updates had already occurred. These edge cases were fascinating because they once again demonstrated that in a distributed system there is no guaranteed order of execution unless it is explicitly enforced.&lt;/p&gt;

&lt;p&gt;We only allowed password reset and account-blocking operations to be synced from the legacy system. We did not provide a way to sync any other updates (i.e., name change), which helped us avoid many edge cases that were almost guaranteed to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Data-Sync System Exhibiting Distributed Database Properties
&lt;/h2&gt;

&lt;p&gt;Based on the migration architecture we have seen, as long as a user was logged in and our reverse proxy could determine their migration status, it sent all their traffic to the new system. For all intents and purposes, the legacy system would appear as “unreachable” to the reverse proxy for this particular user. However, our migration process kept the legacy system up to date, similar to how a primary database instance would keep a standby-primary/replica instance up to date so that it could take over if something went wrong.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff40lpdkua8jlscr2gnvl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff40lpdkua8jlscr2gnvl.webp" alt="Figure 21: Regular update flow for transitioned users" width="800" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, when users were no longer logged in and tried to reset their passwords, the reverse proxy would send all their updates to the legacy system (which was up to date due to backsync). From the reverse proxy’s point of view, it was as if a network partition had made the new system unreachable. After updating its own password copy, the legacy system would sync the update with the new system, ensuring it was up to date, but this would be invisible to the proxy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9t17vmi4omfaw5c8kgft.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9t17vmi4omfaw5c8kgft.webp" alt="Figure 22: Logged out transitioned users’ password getting reset in the legacy system and getting synced with the new system in the background" width="800" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the user resets their password and logs in again, the new system becomes the primary. It was able to fulfil its role as the primary because the legacy system kept it up to date in the background. From the reverse proxy’s point of view, it was as if the network partition had just been recovered, and the old primary was back being the primary, containing all the updates that occurred during the partition.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0v6kl33edozsx3jg102.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0v6kl33edozsx3jg102.webp" alt="Figure 23: Transitioned user logs back in, update flow takes the regular path" width="800" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This example proves that the system we have built to store our users’ data (shown in the above picture with a yellow dotted line) mimics several key behaviors of a Distributed Database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High availability under “partition”: even when the proxy can’t determine a user’s migration status, both reads and writes always succeed.&lt;/li&gt;
&lt;li&gt;Automatic fail-over and recovery: even if a transitioned user’s password reset ends up in the legacy system, it still gets synced to the new system.&lt;/li&gt;
&lt;li&gt;AP system: from the CAP Theorem perspective, it behaves like an AP system — always available and eventually consistent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The entire migration process, from start to finish, had many technical and organizational obstacles. Ultimately, we overcame all obstacles and delivered a robust working solution to the business that did the job perfectly. Our research with Debezium also paid dividends — it helped other teams stream changes for their data migration and &lt;a href="https://adevinta.com/techblog/make-data-migration-easy-with-debezium-and-apache-kafka/" rel="noopener noreferrer"&gt;proved helpful in different use cases&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our architecture has significantly improved since then. We now have a GTID-enabled MySQL cluster with row-based replication, simplifying the overall setup.&lt;/p&gt;

&lt;p&gt;If you are interested in solving complex challenges like what we described here, &lt;a href="https://themen.kleinanzeigen.de/careers/" rel="noopener noreferrer"&gt;we are hiring&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;p&gt;Many thanks to the following KA people who reviewed a draft of this post and provided helpful feedback: Andre Charton, Christiane Lemke, Donato Emma, Joeran Riess, Konrad Campowsky, Louisa Bieler, Max Tritschler, André Charton, Pierre Du Bois, and Valeriia Platonova.&lt;/p&gt;

&lt;p&gt;Special thanks to Sophie Asmus for assisting with the publication.&lt;/p&gt;

&lt;p&gt;Next, kudos to our SREs/DevEx engineers Claudiu-Florin Vasadi, Peter Mucha, Soohyun Kim, Stephen Day, and Wolfgang Braun for supporting us with all things infrastructure.&lt;/p&gt;

&lt;p&gt;Then, many thanks to Matt Goodwin and Robert Brodersen for helping us managing the initiative.&lt;/p&gt;

&lt;p&gt;Finally, a huge thank you to the rest of the Team Red — Christiane Lemke, Franziska Schumann, Maria Sanchez Sierra, Michael Schwalbe, Niklas Lönn, and Valeriia Platonova — whose positive attitudes and hard work turned this challenging migration into a success.&lt;/p&gt;

</description>
      <category>apachekafka</category>
      <category>dataengineering</category>
      <category>streaming</category>
      <category>changedatacapture</category>
    </item>
    <item>
      <title>Building Bridges: How a Team Charter Transformed Our Communication</title>
      <dc:creator>Kleinanzeigen &amp; mobile.de</dc:creator>
      <pubDate>Tue, 10 Jun 2025 09:43:30 +0000</pubDate>
      <link>https://dev.to/berlin-tech-blog/building-bridges-how-a-team-charter-transformed-our-communication-5d5b</link>
      <guid>https://dev.to/berlin-tech-blog/building-bridges-how-a-team-charter-transformed-our-communication-5d5b</guid>
      <description>&lt;p&gt;an article by &lt;a href="https://www.linkedin.com/in/hannaholukoye/" rel="noopener noreferrer"&gt;Hannah Olukoye&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;As an Engineering Manager, I recently led a communication training session for my team in collaboration with an agile coach. Our aim was to examine our interactions more closely, identify areas for improvement, and co-create a team communication charter.&lt;/p&gt;

&lt;p&gt;The session proved to be an incredibly valuable experience, helping us foster stronger alignment, mutual understanding, and a clearer framework for our work together.&lt;/p&gt;

&lt;p&gt;In this article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I will share key learnings and helpful tips through our team experience&lt;/li&gt;
&lt;li&gt;I will share links to a template we used in our team to guide the discussions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re considering a similar initiative with your team, I hope our journey offers both inspiration and a practical starting point to help foster more intentional, effective communication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting the Mood&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I planned this communication training session for our team — with the support of an agile coach, I wasn’t entirely sure what to expect. We knew we wanted to improve how we interacted, especially in moments of tension, but what emerged from the session far exceeded those intentions. The room quickly shifted into a space of openness, curiosity, and reflection as we dove into what truly drives — and derails — effective communication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unpacking the Discoveries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the most eye-opening exercises was exploring how we each tend to communicate in conflict. Guided by our agile coach, we examined four core styles: &lt;strong&gt;Aggressive&lt;/strong&gt;, &lt;strong&gt;Passive&lt;/strong&gt;, &lt;strong&gt;Assertive&lt;/strong&gt;, and &lt;strong&gt;Passive-Aggressive&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;AGGRESSIVE&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dominates&lt;/li&gt;
&lt;li&gt;Interrupts&lt;/li&gt;
&lt;li&gt;Ignores opinions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ASSERTIVE&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Speaks clearly&lt;/li&gt;
&lt;li&gt;Advocates for self &amp;amp; others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PASSIVE-AGGRESSIVE&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mixed signals
&lt;/li&gt;
&lt;li&gt;Indirect&lt;/li&gt;
&lt;li&gt;Confusing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PASSIVE&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoids conflict&lt;/li&gt;
&lt;li&gt;Doesn't speak up&lt;/li&gt;
&lt;li&gt;Prioritizes others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We saw how aggressive communicators can appear confident but often override others’ voices. Passive communicators avoid confrontation, sometimes to the detriment of clarity. Passive-aggressive behaviour sits in the murky middle — indirect, and often confusing.&lt;/p&gt;

&lt;p&gt;But the real breakthrough came with assertiveness: direct, clear, and respectful communication that makes space for both expression and listening. It became our shared ideal.&lt;/p&gt;

&lt;p&gt;In another exercise, we mapped out our individual communication profiles: &lt;strong&gt;Analytical&lt;/strong&gt;, &lt;strong&gt;Amiable&lt;/strong&gt;, &lt;strong&gt;Expressive&lt;/strong&gt;, and &lt;strong&gt;Driver&lt;/strong&gt;. Each style brought its strengths and challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Analyticals value logic and precision, but may be slow to act or overly critical.&lt;/li&gt;
&lt;li&gt;Amiables foster harmony and empathy, yet can shy away from necessary conflict.&lt;/li&gt;
&lt;li&gt;Expressives infuse energy and creativity, though they may lack follow-through.&lt;/li&gt;
&lt;li&gt;Drivers are focused and decisive, but risk being perceived as overly forceful.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;[ ANALYTICAL ]&lt;br&gt;
🧩 Logic-driven&lt;br&gt;
⚠️ Critical&lt;/p&gt;

&lt;p&gt;[ AMIABLE ]&lt;br&gt;
💙 People-first&lt;br&gt;
⚠️ Over-accommodating&lt;/p&gt;

&lt;p&gt;[ EXPRESSIVE ] &lt;br&gt;
🌟 Creative buzz&lt;br&gt;
⚠️ Lacks detail&lt;/p&gt;

&lt;p&gt;[ DRIVER ]&lt;br&gt;
⚡ Results focus&lt;br&gt;
⚠️ Too pushy&lt;/p&gt;

&lt;p&gt;As we explored these styles, we realised much of our past communication friction wasn’t due to misalignment, but rather a clash of unspoken styles. It wasn’t dysfunction — it was difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Putting Everything Together&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By the end of the session, the fog had lifted. We understood more about how each of us communicates — especially under stress, and how to adjust our approach with others in mind. These insights became the bedrock for our Team Canvas: a set of shared, written agreements that define how we want to show up in conversations, give feedback, and navigate conflict.&lt;/p&gt;

&lt;p&gt;It’s not a perfect science, but it’s a huge step forward. Now, when tension arises, we don’t just react — we pause, remember what we’ve agreed on, and approach each other with more empathy and intention.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fby7cgk6tp8juzsdrx6xh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fby7cgk6tp8juzsdrx6xh.webp" alt="Image description" width="800" height="532"&gt;&lt;/a&gt; &lt;em&gt;Template from TheTeamCanvas Website&lt;/em&gt;&lt;/p&gt;

&lt;center&gt;. . . &lt;/center&gt;

</description>
      <category>team</category>
      <category>leadership</category>
      <category>collaboration</category>
      <category>communication</category>
    </item>
    <item>
      <title>Understanding and Resolving Infinite Consumer Lag Growth on Compacted Kafka Topics</title>
      <dc:creator>Kleinanzeigen &amp; mobile.de</dc:creator>
      <pubDate>Tue, 25 Jun 2024 08:55:42 +0000</pubDate>
      <link>https://dev.to/berlin-tech-blog/understanding-and-resolving-infinite-consumer-lag-growth-on-compacted-kafka-topics-787</link>
      <guid>https://dev.to/berlin-tech-blog/understanding-and-resolving-infinite-consumer-lag-growth-on-compacted-kafka-topics-787</guid>
      <description>&lt;p&gt;&lt;em&gt;an article by &lt;a href="https://www.linkedin.com/in/andrecharton/" rel="noopener noreferrer"&gt;André Charton&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Kleinanzeigen has been using Kafka since 2016 as a distributed streaming platform of choice. We have many real-time data pipelines and streaming applications running on top. Some of our topics are compacted...&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a compacted topic?&lt;/strong&gt;&lt;br&gt;
A compacted topic in Apache Kafka is a special type of topic where Kafka’s log compaction feature is enabled. It helps retain the latest records for each key in the topic while removing older records for the same key. This pattern we apply for topics in front of our ElasticSearch indices, so we can use it as a scalable source of truth to index and also full index.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1fmuotdun2sjvxlaryw.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1fmuotdun2sjvxlaryw.jpeg" alt="Image description" width="800" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is consumer lag?&lt;/strong&gt;&lt;br&gt;
Consumer lag is a metric that measures how far behind a consumer is from the latest message in a Kafka topic/partition. It holds the number of messages that the consumer needs to process. Sometimes we see a lag increase, while an application bottlenecks, on network issues, etc.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkqysahxjarsz0d7d7km.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkqysahxjarsz0d7d7km.jpeg" alt="Image description" width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Per default monitoring consumer lag ensures that consumers are keeping up with the producers. We expose this metric for our clusters and have it in Prometheus, visualised in Grafana.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is an offset reset?&lt;/strong&gt;&lt;br&gt;
In Apache Kafka, an offset reset refers to the operation of changing the current offset position for a consumer group. The offset determines the position from which the consumer will start reading records from a partition. This strategy we can perfectly use to execute a full index on our indices, described above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why infinitive growth?&lt;/strong&gt;&lt;br&gt;
Since we using Kafka 8+ years, some topics getting older and older. A compacted topic for instance containing user posted ads (used by full index our major search index). With the years we see on full index operation the lag is getting bigger and bigger. Recently we saw numbers above 400M. We wondered, getting nervous and invested. But it happens by the nature of combing a compacted topic and offset reset.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjpjhg7popbxqx5fjxh3p.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjpjhg7popbxqx5fjxh3p.jpeg" alt="Image description" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Over time the distance between “now” and the oldest record will growth until the oldest record is gone. We have some user ads from even before 2016, because user can extend ad lifetime again and again. So when we perform an offset reset, a consumer will start at the beginning: [0], in the sample below at [2]. Our log metric would show a lag of [8] still it just needs to produce 3 records. So this explains the spike we saw in Grafana metric which measures “just” the offset.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5tuaejtc5jh3euaizna9.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5tuaejtc5jh3euaizna9.jpeg" alt="Image description" width="542" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
Be careful on the interpretation of lag metrics on compacted topics in case of offset reset. In our example of a full index and lag of 400M, we count just less than 60M records get processed.&lt;/p&gt;

&lt;p&gt;Another option could be to rewrite the topic using MirrorMaker and a new topic name. But we are fine with understanding here.&lt;/p&gt;

&lt;p&gt;Special thanks to my colleague &lt;a href="https://www.linkedin.com/in/daniil-roman/" rel="noopener noreferrer"&gt;Daniil Roman&lt;/a&gt; who inspired me to this article.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>compaction</category>
    </item>
    <item>
      <title>“Data has a Dream” — A Short comic about data mesh and how it can transform your company</title>
      <dc:creator>Kleinanzeigen &amp; mobile.de</dc:creator>
      <pubDate>Mon, 18 Mar 2024 19:42:49 +0000</pubDate>
      <link>https://dev.to/berlin-tech-blog/data-has-a-dream-a-short-comic-about-data-mesh-and-how-it-can-transform-your-company-2b38</link>
      <guid>https://dev.to/berlin-tech-blog/data-has-a-dream-a-short-comic-about-data-mesh-and-how-it-can-transform-your-company-2b38</guid>
      <description>&lt;p&gt;&lt;em&gt;a little data story by &lt;a href="https://www.linkedin.com/in/schuelermarkus/" rel="noopener noreferrer"&gt;Markus Schüler&lt;/a&gt; (Director of Data Strategy Adevinta) with drawings by &lt;a href="https://www.linkedin.com/in/gitanjalivenkatraman/" rel="noopener noreferrer"&gt;Gitanjali Venkatraman&lt;/a&gt; (Technology Writer and Illustrator at ThoughtWorks)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The profound impact of Data Mesh and its associated principles&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;domain data ownership&lt;/li&gt;
&lt;li&gt;data as a product&lt;/li&gt;
&lt;li&gt;self-serve data platform&lt;/li&gt;
&lt;li&gt;federalised data governance processes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;are currently reshaping our industry landscape. Amidst this revolution, the most important realisation at mobile.de was that the key to a successful Data Mesh implementation was bringing our people along on that journey — no matter if that is the C-level leadership team or members of our product and tech teams. And for that we need to convey the benefits of data mesh in simple terms, avoiding the pitfalls of cryptic data terminology.&lt;/p&gt;

&lt;p&gt;Out of this realisation, a whimsical idea was born — a comic that breaks down these fundamental principles and illustrates their transformative power but in a unique and accessible way. Teaming up with the passionate Data Mesh enthusiasts at &lt;a href="https://www.thoughtworks.com/" rel="noopener noreferrer"&gt;Thoughtworks&lt;/a&gt;, where the concept of Data Mesh first came to life, we are thrilled to present “Data has a Dream” our very own Data Mesh comic:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/0KCZv9zNb4U"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;A heartfelt thank you goes out to the individuals who made this possible: Special appreciation to Gitanjali Venkatraman for infusing life into little data’s journey with her incredible drawings and amazing skill to simplify complex terms. Also to Chris Ford and Magno Mathias for believing in my seemingly crazy idea and tuning it into a tangible reality. And finally to my amazing teams at mobile.de and Adevinta, who made me embark on the journey of learning more about data mesh through them.&lt;/p&gt;

</description>
      <category>data</category>
      <category>datamesh</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Better Search Relevance using Learning To Rank at mobile.de</title>
      <dc:creator>Kleinanzeigen &amp; mobile.de</dc:creator>
      <pubDate>Thu, 29 Feb 2024 11:16:54 +0000</pubDate>
      <link>https://dev.to/berlin-tech-blog/better-search-relevance-using-learning-to-rank-at-mobilede-2l9k</link>
      <guid>https://dev.to/berlin-tech-blog/better-search-relevance-using-learning-to-rank-at-mobilede-2l9k</guid>
      <description>&lt;p&gt;&lt;em&gt;Written by Manish Saraswat&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At mobile.de, we continuously strive to provide our users with a better, faster and a unique search experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5cdqh5w0y3qlnijwybq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5cdqh5w0y3qlnijwybq.png" width="800" height="554"&gt;&lt;/a&gt;showing mobile.de search engine&lt;/p&gt;

&lt;p&gt;Every day, millions of people visit mobile.de to find their dream car. The user journey typically starts by entering a search query and later refining it based on their requirements. If the user finds a relevant listing, they contact the seller to purchase the vehicle. Our search engine is responsible for matching users with the right sellers.&lt;/p&gt;

&lt;p&gt;With over 1 million listings to display, finding the top 100 relevant results within a few milliseconds is an immense challenge. Not only do we need to ensure the listings match the user’s search intent, but we also must honour the exposure guarantees made to our premium dealers in their sales packages.&lt;/p&gt;

&lt;p&gt;Identifying the ideal search results from over 1 million listings quickly while optimising for user relevance and business commitments requires an intricate balancing act.&lt;/p&gt;

&lt;p&gt;In this post, I would like to share how we are building learning to rank models and deploying them in our infrastructure using a python microservice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What motivated us?
&lt;/h2&gt;

&lt;p&gt;Our current Learning to Rank (LTR) system is integrated into our ElasticSearch cluster using the native ranking plugin. This plugin offers a scalable solution to deploy learning to rank models out-of-the-box.&lt;/p&gt;

&lt;p&gt;While it has provided a solid foundation over several years, we have encountered some limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Our DevOps team faced plugin integration issues when upgrading ElasticSearch versions&lt;/li&gt;
&lt;li&gt;There is no automated model deployment, requiring manual pre-deployment checks by our data scientists. This introduces risks of human error.&lt;/li&gt;
&lt;li&gt;Overall system maintenance has become difficult&lt;/li&gt;
&lt;li&gt;The infrastructure bottlenecks limit our data scientists from testing newer ML models that could improve relevance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clearly, while the native &lt;strong&gt;ElasticSearch&lt;/strong&gt; ranking plugin gave us an initial working solution, it has become an obstacle for iterating and improving our LTR capabilities. We realised the need to evolve to a more scalable, automated and flexible LTR architecture.&lt;/p&gt;

&lt;p&gt;This would empower our data scientists to rapidly experiment with more advanced ranking algorithms while enabling easier system maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did we start?
&lt;/h2&gt;

&lt;p&gt;Realising our &lt;strong&gt;outdated search architecture&lt;/strong&gt; was the primary obstacle to improving relevance, we knew a pioneering solution was needed to overcome this roadblock.&lt;/p&gt;

&lt;p&gt;We initiated technical discussions with Site Reliability Engineers, Principal Backend Engineers and Product Managers to assess how revamping search could impact website experience.&lt;/p&gt;

&lt;p&gt;Our solution had to balance speed with business metrics. We needed to keep search fast while improving key conversions like unique user conversion rate.&lt;/p&gt;

&lt;p&gt;Based on the feedback, we decided to decouple the relevance algorithm into a &lt;strong&gt;separate microservice&lt;/strong&gt;. To empower data scientists and engineers, we chose Python to align development and production environments closely while ensuring scalability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing Learning to Rank
&lt;/h2&gt;

&lt;p&gt;There are several techniques to implement learning to rank (LTR) models in python. Up until a few years ago, we were using a &lt;strong&gt;pointwise ranking&lt;/strong&gt; approach, which worked well for us.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feagfdh9ovgwav41mppbh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feagfdh9ovgwav41mppbh.png" width="269" height="103"&gt;&lt;/a&gt;showing pointwise loss&lt;/p&gt;

&lt;p&gt;Last year, we decided to test a &lt;strong&gt;pairwise ranking&lt;/strong&gt; model (trained using XGBoost) against the pointwise model and it outperformed in the A/B test.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fig20kbid4xlnp7uhdj5v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fig20kbid4xlnp7uhdj5v.png" width="355" height="69"&gt;&lt;/a&gt;showing pairwise loss&lt;/p&gt;

&lt;p&gt;This gave us good confidence to continue using the pairwise ranking approach. Also, the latest &lt;strong&gt;XGBoost&lt;/strong&gt; version (&amp;gt;=2.0) provides lots of cool features such as handling position bias options while training the model. Also, since XGBoost supports using custom loss function, we trained the model using a &lt;strong&gt;multi objective loss function&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In our case, our objectives are set to listing relevance and dealer exposure. As mentioned above, we try to optimise the balance between showing relevant results and showing our premium/sponsored dealers at top positions.&lt;/p&gt;

&lt;p&gt;Training the models in the &lt;a href="https://jupyter.org/" rel="noopener noreferrer"&gt;jupyter notebook&lt;/a&gt; is the easy part. We can use all the features we need and build a model. However, as a data scientist, we should always ask ourselves, will these features be available in production? Approaching a machine learning (ML) model from a product perspective helps to tackle lots of problems in advance.&lt;/p&gt;

&lt;p&gt;Keeping the features feasibility in mind, we decided to test the model with following raw and derived features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Historical performance of the listing&lt;/li&gt;
&lt;li&gt;Historical performance of the seller&lt;/li&gt;
&lt;li&gt;Listing attributes (make, model, price, rating, location etc)&lt;/li&gt;
&lt;li&gt;Freshness of the listing&lt;/li&gt;
&lt;li&gt;Age of the listing (based on registration date)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When tested offline using &lt;strong&gt;NDCG@k&lt;/strong&gt; metric, we found that these features gave us a good uplift as compared to the existing model. We always aim for uplift in offline metrics before testing the model online in an &lt;strong&gt;A/B test&lt;/strong&gt;, this helps us to iterate faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  How did we serve the models?
&lt;/h2&gt;

&lt;p&gt;We learnt that serving a machine learning model has multiple aspects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensuring the model has access to features array to predict&lt;/li&gt;
&lt;li&gt;Ensuring the model is trained periodically to learn the latest trends in the business&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To tackle the above aspects, we used &lt;a href="https://airflow.apache.org/" rel="noopener noreferrer"&gt;airflow&lt;/a&gt; to schedule our ETL jobs to calculate features. Due to the choice of our features, we were able to precompute the feature vector and store it in a feature store. To summarise, we had to setup following jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;To fetch latest information, every new update of a listing is pushed to a kafka stream, we consume this stream using a python service to update our feature array&lt;/li&gt;
&lt;li&gt;Another task reads these updated feature arrays, generates prediction and store them into our feature store&lt;/li&gt;
&lt;li&gt;Training job retrains the model once a week based on optimised set of parameters, adds versioning to the model and stores it in gcp bucket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh4r7z1xdvt2cge8yrnrl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh4r7z1xdvt2cge8yrnrl.png" width="800" height="530"&gt;&lt;/a&gt;showing data pipelines for model training and feature generation&lt;/p&gt;

&lt;p&gt;We created a microservice (API) using &lt;a href="https://fastapi.tiangolo.com/" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt; to serve the models. You might ask why not &lt;a href="https://flask.palletsprojects.com/en/3.0.x/" rel="noopener noreferrer"&gt;Flask&lt;/a&gt;? We have been using FastAPI for quite some time now and haven’t found any bottleneck yet to think about other frameworks. Also, FastAPI framework has quite solid documentation where they also share the best practices to build an API.&lt;/p&gt;

&lt;p&gt;Our service workflow looks like the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The development work for FastAPI happened in Python.&lt;/li&gt;
&lt;li&gt;The code gets pushed to github. Using CI pipelines integrated with linting test, unit testing, integration testing we make sure every new line of code pushed is tested. Also, the code gets packaged into a docker image and gets pushed to a registry.&lt;/li&gt;
&lt;li&gt;Deploy the docker image on &lt;a href="https://kubernetes.io/" rel="noopener noreferrer"&gt;kubernetes&lt;/a&gt; (although this part is mainly handled by our site ops team).&lt;/li&gt;
&lt;li&gt;Track the service health metrics using &lt;a href="https://grafana.com/" rel="noopener noreferrer"&gt;grafana&lt;/a&gt; dashboards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqcuo2z4h1sfkhyi3b4pm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqcuo2z4h1sfkhyi3b4pm.png" width="800" height="408"&gt;&lt;/a&gt;showing ranking service latency over 24 hours&lt;/p&gt;

&lt;h2&gt;
  
  
  Show me the results
&lt;/h2&gt;

&lt;p&gt;We were also waiting to see if our months of hard work was going to make an impact. We decided to launch an A/B test for two weeks. At mobile.de, the best part of being a data scientist is that you are involved in the end to end process.&lt;/p&gt;

&lt;p&gt;After putting all the pieces together, we launched an A/B test for two weeks and recorded positive significant improvements in the business metrics. For example, while not affecting the SRP (search result page) performance — microservice responding under 30 milliseconds at p99, the new search relevance algorithm generated:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fti3bwg4lb2aic89xnl0m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fti3bwg4lb2aic89xnl0m.png" width="800" height="342"&gt;&lt;/a&gt;showing change in metrics post A/B test&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmwm7aba7yxwergg2y0ii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmwm7aba7yxwergg2y0ii.png" width="800" height="280"&gt;&lt;/a&gt;showing user replies and parking buttons on car listings&lt;/p&gt;

&lt;p&gt;This uplift is special for the team because the baseline we were competing against was already providing solid results. Given the significant uplifts in our metrics we strongly believe that the team has done a tremendous job in improving the search relevance for our users. That is, making it easier for our users to find the right vehicle and contact the seller.&lt;/p&gt;

&lt;h2&gt;
  
  
  End Notes
&lt;/h2&gt;

&lt;p&gt;In this post, I shared our experience building learning to rank models and serving them using a microservice in python. The idea here was to give you a high level overview of the different aspects we touched during this project.&lt;/p&gt;

&lt;p&gt;All of this would have not been possible without an incredible team. Special thanks to &lt;strong&gt;Alex Thurm, Melanya Bidzyan, Stefan Elmlinger&lt;/strong&gt; for contributing to this project at different stages.&lt;/p&gt;

&lt;p&gt;In case you have questions/suggestions, feel free to write them below in the comments section. Stay tuned for more stories :)&lt;/p&gt;

</description>
      <category>xgboost</category>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>search</category>
    </item>
    <item>
      <title>Embracing Growth and Learning: My Journey as a Software Developer Trainee</title>
      <dc:creator>Kleinanzeigen &amp; mobile.de</dc:creator>
      <pubDate>Mon, 07 Aug 2023 14:56:55 +0000</pubDate>
      <link>https://dev.to/berlin-tech-blog/embracing-growth-and-learning-my-journey-as-a-software-developer-trainee-304h</link>
      <guid>https://dev.to/berlin-tech-blog/embracing-growth-and-learning-my-journey-as-a-software-developer-trainee-304h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Hello, I’m Simona, and I’m thrilled to share my journey as a Software Developer Trainee at mobile.de (part of Adevinta). As a passionate learner and adventurer, I’ve ventured on an incredible path of growth and transformation. With a background in hospitality management and a newfound passion for web development, I’ve discovered a world of possibilities at the Trainee programme. Join me as I reflect on my experiences, both past and present, and the invaluable learning opportunities I’ve encountered along the way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl27h9w2ze8by6rc4axvg.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl27h9w2ze8by6rc4axvg.JPG" width="800" height="1066"&gt;&lt;/a&gt;That’s me, Simona.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nurturing Curiosity through Coding&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Born and raised in Lithuania, my adventurous spirit led me on a journey far from home. I studied Archaeology in university, always driven by a curious nature and a thirst for new experiences. Over the past decade, I’ve lived in six different countries, immersing myself in different cultures and working in hospitality management. These experiences taught me resilience, adaptability, and the art of building a home from scratch in unfamiliar places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Turning Point&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;However, there came a point when I realised that the places I lived no longer challenged me enough. I was craving something more, something that could ignite my passion and drive. Little did I know that the answer would lie in the world of technology. Despite not having a background in IT, I’ve always had a fascination with the field. In my youth, I was drawn to mathematics and dreamed of the possibilities of coding, but societal norms at the time discouraged me from pursuing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An Unexpected Passion Unveiled&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During the COVID-19 pandemic, I stumbled upon an online marketing and web development course. Intrigued by the potential connections with my hospitality background, I enrolled. It turned out that this accidental discovery would reveal a long-suppressed passion. As I delved into coding, I became captivated by its intricacies and limitless possibilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embracing Change&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Driven by a newfound fascination, I made the bold decision to transition into a career in web development. Despite the doubts that crept in about my late entry into the field and societal biases, I chose to embrace the challenge. I enrolled in a one-year web development coding bootcamp and dedicated myself wholeheartedly to learning this exciting new craft. The journey was not without its difficulties, but the satisfaction of conquering challenges and the joy of finding creative solutions to complex problems fuelled my determination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Joining mobile.de, part of Adevinta&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Today, I am proud to be a part of &lt;a href="https://www.mobile.de/careers/" rel="noopener noreferrer"&gt;mobile.de&lt;/a&gt; and so &lt;a href="https://www.adevinta.de/career-opportunities" rel="noopener noreferrer"&gt;Adevinta&lt;/a&gt;. As a Software Developer Trainee, I have found a supportive and inclusive community that values diversity and fosters personal growth. Adevinta’s commitment to include everyone, especially empowering women in tech, resonates deeply with me. I want to inspire other women to break barriers, challenge societal norms, and pursue their passions fearlessly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcgjld1e0q4bo39sw5uag.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcgjld1e0q4bo39sw5uag.jpg" width="800" height="450"&gt;&lt;/a&gt;Our trainee cohort 2022 during their onboarding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contributing to a Trusty Digital Space&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my role as a frontend engineer with mobile.de’s Trust &amp;amp; Safety team, I have the privilege of working alongside experienced professionals on meaningful projects. During my first rotation, I have been actively involved in developing and enhancing features that promote trust within our platform. It’s truly fulfilling to know that our team work contributes to establishing a secure and reliable digital space for our users.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flahdhjbs55bgu7t5bcw4.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flahdhjbs55bgu7t5bcw4.JPG" width="800" height="600"&gt;&lt;/a&gt;Our Trust &amp;amp; Safety team at a team building event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inclusive Collaboration, Agile Practices, and Mentorship&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my experience at mobile.de, collaboration, agile practices, and mentorship are highly valued. The company promotes a culture of continuous learning and collaboration, where agile methodologies are embraced in our development processes. We have &lt;a href="https://dev.to/berlin-tech-blog/celebrating-a-decade-of-creativity-337k"&gt;quarterly innovation days&lt;/a&gt; that allow employees from all departments to pitch ideas and collaborate, fostering creativity and cross-team cooperation for innovative solutions. Weekly Frontend Guild meetings ensure alignment, share best practices, and keep our code base up to date, creating an efficient and cohesive development environment.&lt;br&gt;
One aspect that has greatly contributed to my personal and professional growth is the regular one-on-one meetings with my assignment lead and mentorship in pair programming sessions. These interactions have not only enhanced my problem-solving skills but also provided valuable guidance and support whenever I’ve felt overwhelmed or stuck. Additionally, the regular retrospectives provide an inclusive platform for every colleague to voice their opinions, enabling us to identify areas of improvement and make necessary adjustments to enhance our work-life balance and productivity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community Building and Talent Development&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Adevinta prioritises community building and talent development, exemplified through events like the Early Careers conference where I had a pleasure to attend. This conference brought together trainees and early-career professionals from various locations, providing workshops focused on talent development and opportunities to share personal and professional experiences. It not only expanded networks but also enriched perspectives on the broader level of the company, fostering connections and growth within the community.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3cwknu8fm8wjuzyy2yvt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3cwknu8fm8wjuzyy2yvt.jpg" width="800" height="534"&gt;&lt;/a&gt;Snapshot from the Adevinta Early Careers conference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embracing the Journey&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As I continue my journey as a Software Developer Trainee, I am constantly reminded of the importance of embracing growth and learning. Every day brings new challenges and opportunities to expand my knowledge and skill set. I am grateful for the supportive mentor, manager and a whole network of colleagues who are always willing to share their expertise, encourage to explore and help me overcome obstacles.&lt;/p&gt;

&lt;p&gt;My experience as a Trainee has been transformative. From my diverse background in hospitality to my newfound passion for web development, I have grown both personally and professionally. Adevinta and mobile.de has provided me with a platform to pursue my goals, embrace change, and inspire others to join the dynamic field of technology. I encourage everyone, regardless of their gender, background, or age, to never shy away from pursuing their passions in tech. With Adevinta’s great example and unwavering commitment to diversity, inclusivity, and personal growth, the possibilities are limitless.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwabuc9dry7cgzimhd6d8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwabuc9dry7cgzimhd6d8.jpg" width="800" height="600"&gt;&lt;/a&gt;I will never shy away!&lt;/p&gt;

</description>
      <category>trainee</category>
      <category>codingbootcamp</category>
      <category>talent</category>
      <category>growth</category>
    </item>
    <item>
      <title>Hadoop Migration: How we pulled this off together</title>
      <dc:creator>Kleinanzeigen &amp; mobile.de</dc:creator>
      <pubDate>Sun, 16 Apr 2023 11:00:15 +0000</pubDate>
      <link>https://dev.to/berlin-tech-blog/hadoop-migration-how-we-pulled-this-off-together-28a6</link>
      <guid>https://dev.to/berlin-tech-blog/hadoop-migration-how-we-pulled-this-off-together-28a6</guid>
      <description>&lt;p&gt;&lt;em&gt;A short guide to help understand the process of migrating old analytical data pipelines to AWS by following the Data Mesh strategy.&lt;br&gt;
by Aydan Rende, Senior Data Engineer at eBay Kleinanzeigen&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hadoop was used as a data warehouse in a few marketplaces in the former eBay Classifieds Group (now part of Adevinta) including eBay Kleinanzeigen for a long time. While it served analytical purposes well, the central teams wanted to say goodbye to this old friend. The reason was simple: it was old and costly.&lt;/p&gt;

&lt;p&gt;Before diving into the solution, let’s take a look at eBay Kleinanzeigen’s Hadoop data pipeline:&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2gs8ecfd1k9j1mtx47u.jpg" width="571" height="221"&gt;Deprecated Hadoop data pipeline&lt;br&gt;


&lt;ol&gt;
&lt;li&gt;The monolith is the main backend service of eBay Kleinanzeigen. It has several Kafka topics and produces analytical events in JSON format to the Kafka Cluster.&lt;/li&gt;
&lt;li&gt;These topics are consumed and ingested to Hadoop by the Flume Ingestor.&lt;/li&gt;
&lt;li&gt;The monolith runs scheduled jobs every midnight to fetch and aggregate the analytical data.&lt;/li&gt;
&lt;li&gt;Finally these aggregates are stored in KTableau which is a MySQL database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I have marked the problematic components of this pipeline in dark magenta:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hadoop is not maintained by Cloudera and runs as an old version, which means that the maintenance costs extra.&lt;/li&gt;
&lt;li&gt;Kafka cluster is on-prem and again in the old version (v1). We had a strict deadline from the DevOps team to shut down the cluster because the hardware reached its end of life.&lt;/li&gt;
&lt;li&gt;KTableau is not a Tableau instance, it's a non-maintained on-prem MySQL. I have marked this in pink because this is the next one to get rid of. (K-Tableau: K comes from Kleinanzeigen)&lt;/li&gt;
&lt;li&gt;On-prem monolithic service is the main serving point of the eBay Kleinanzeigen platform. It's a bottleneck, however. The service also runs analytical jobs, but mostly fails silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problems I have outlined give good reasons to change the data setup, as the entire company is, as a matter of fact, in the process of cleaning up and moving away from on-prem to AWS, from monolith to microservices etc. So why not clean up the old analytical data pipeline as well? Yet, we had a teeny tiny issue to deal with; our Data Team was relatively small, so the question was „How do we pull this off together in a short time?"&lt;/p&gt;
&lt;h2&gt;
  
  
  Data Mesh to the Rescue
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.datamesh-architecture.com/" rel="noopener noreferrer"&gt;Data Mesh&lt;/a&gt; is a decentralised data management paradigm that allows teams to create their own data products suited to the company policies by using a central data infrastructure platform. This paradigm aligns with Domain Driven Design which eBay Kleinanzeigen successfully implements for the teams. The teams own a domain and they can also own domain data products as well.&lt;/p&gt;

&lt;p&gt;Data Mesh is not new to Adevinta (our parent company). Adevinta's central teams already provide a self-serve data platform called DataHub and the marketplaces use this platform autonomously. It has several managed data solutions from data ingestion to data governance. Our task was to learn and create a new data pipeline with these services. However, we also wanted to use dbt for the transformation layer of the ETL process in addition to the services provided because we wanted to keep the transformation layer neat and versioned.&lt;/p&gt;

&lt;p&gt;This migration seems to be more important because it's the beginning of the Data Mesh strategy at eBay Kleinanzeigen. It's great that our teams already own domains, but owning data products is new to them. Therefore, we decided to create a proof of concept, migrate the existing datasets from Hadoop to the new design and explain to teams the ownership of data.&lt;/p&gt;
&lt;h2&gt;
  
  
  The New Design
&lt;/h2&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcismzb0livvwgljo4g3b.jpg" width="561" height="411"&gt;Data pipeline of a domain&lt;br&gt;


&lt;p&gt;The new design looks more complicated, but, in fact, it's easier to adopt by the teams, as they can reach out to the central services and integrate with the entire data ecosystem that exists in Adevinta. Hence, it provides data ownership out of the box.&lt;/p&gt;

&lt;p&gt;In the new design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The backend services already use Kafka to emit events, however, the new Kafka cluster is on AWS and is „managed" which means that maintenance of the cluster is taken care of by the central teams.&lt;/li&gt;
&lt;li&gt;Scheduled jobs are run in Airflow in a more resilient way. It's better to trace the logs and get notified of errors on Airflow. We no longer need to dive into logs of the big monolith backend service where it is polluted by the other service logs.&lt;/li&gt;
&lt;li&gt;Data transformation is performed in dbt instead of the backend services. Data analysts can go to the dbt repository and check the SQL queries instead of reading through the backend service code to understand the reporting query.&lt;/li&gt;
&lt;li&gt;We leverage the central services as much as possible to reduce the DevOps effort and costs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these changes, we not only deprecate the old Hadoop instance, but also take the analytics load away from the backend services, which are supposed to be busy with the business transactions anyway, not the analytical transactions.&lt;/p&gt;
&lt;h2&gt;
  
  
  Managed Kafka
&lt;/h2&gt;

&lt;p&gt;Managed Kafka is a data streaming solution that is an AWS Kafka Cluster and is owned by the Adevinta Storage Team. The central team offers maintained secure Kafka Clusters, provides metrics and on-call services. All we need to do is create new Kafka topics to replace the old Kafka topics running on-prem. We have also changed the record type: it was JSON in the old setup, but we decided to use AVRO to have schemas available in the repositories with the version control system (Github in our case).&lt;/p&gt;


  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8gjrl3xdcilm5qti5741.jpg" width="800" height="484"&gt;Metrics of the Managed Kafka Cluster&lt;br&gt;

&lt;h2&gt;
  
  
  DataHub Sink
&lt;/h2&gt;

&lt;p&gt;Sink is an in-house event router that consumes Kafka topics, transforms, filters events and stores them inside the S3 bucket or another Managed Kafka topic. In this phase, we collect the raw data, convert it to Delta format and store it to our AWS S3 bucket with a sink. Delta format gives us ACID (Atomicity, Consistency, Isolation, and Durability) properties that guarantee a consistent version of the tables at any read time, even in case of concurrent writes. It thus avoids inconsistent or incomplete reads.&lt;/p&gt;
&lt;h2&gt;
  
  
  Databricks
&lt;/h2&gt;

&lt;p&gt;Databricks is an analytical data service that provides data lake &amp;amp; data warehouse capabilities together in one platform. This was not an ideal choice for our setup, if you consider that we already have an AWS Data Lake. Databricks is not offered by Datahub, but by another central team. It has already been used by our data analysts, so we tried to stick with that and mounted Databricks to our S3 bucket instead. Once the delta files are collected under the S3 path, we create a table in Databricks. You can read more about the mounting in &lt;a href="https://docs.databricks.com/storage/amazon-s3.html" rel="noopener noreferrer"&gt;this document&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Data Build Tool (dbt)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.getdbt.com/docs/introduction" rel="noopener noreferrer"&gt;dbt&lt;/a&gt; is a data transformation tool that enables data analysts and scientists to run transformation workflows while benefiting from the software engineering practices such as collaboration on data models, versioning them, testing and documentation. dbt also provides a lineage graph between fact and dimension tables so that dependencies can be visualised in the document generated.&lt;/p&gt;

&lt;p&gt;We created a dbt repository that has several SQL models and is integrated with Databricks. We implemented the CI/CD pipeline with Github actions so that every time we release a new model in dbt, a docker image is created together with the entire dbt repository, secrets and dbt profile and then this image is pushed to Artifactory. The image is later fetched by the Airflow operator and is run in a schedule. Another great feature of dbt is that we can easily switch the warehouse setup from Databricks to Redshift in the future by making only a few changes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Airflow
&lt;/h2&gt;

&lt;p&gt;Airflow is a great job orchestration tool and a managed version of Airflow is offered by Adevinta central teams. Managed Airflow is a managed Kubernetes cluster that comes with the Airflow service and a few operators configured out of the box. In the managed cluster, it is difficult for us to install packages on our own. We need to request this from the owning team. We are also not the only tenants in the cluster, which means that, even if the central team agrees to install the required packages, a package conflict can affect the other tenants. That's why we decided to run dbt within a docker container with the KubernetesPodOperator. It's also a best practice to containerise as much as possible due to Airlflow's instabilities, which are described &lt;a href="https://medium.com/bluecore-engineering/were-all-using-airflow-wrong-and-how-to-fix-it-a56f14cb0753" rel="noopener noreferrer"&gt;in this blog post&lt;/a&gt; in more detail. KubernetesPodOperator instantiates a pod to run your image within the Kubernetes cluster. This gives us the ability to create an isolated environment so that we can install whatever dependency we want to in order to execute the dbt command.&lt;/p&gt;

&lt;p&gt;Here is an example of a DAG in Airflow that we executed to produce a data mart sent by email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;namespace = Variable.get("UNICRON_NAMESPACE", default_var="NO_NAMESPACE")
environment = Variable.get("UNICRON_USER_ENV", default_var="NO_ENV")
docker_image = Variable.get("DBT_DOCKER_IMAGE", default_var="NO_IMAGE")

default_args = {
   'owner': 'det',
   'depends_on_past': False,
   'email': ['some.email@adevinta.com'],
   'email_on_failure': True,
   'email_on_retry': False,
   'retries': 3,
   'retry_delay': timedelta(minutes=5),
}
with DAG(
       dag_id=os.path.basename(__file__).replace(".py", ""),
       default_args=default_args,
       description='Generates Kleinanzeigen email send-out analytics report for the last day',
       schedule_interval="0 3 * * *",
       is_paused_upon_creation=False,
       start_date=datetime(2023, 1, 3),
       on_failure_callback=send_dag_failure_notification,
       catchup=False,
       tags=['belen', 'analytics', 'email_sent', 'dbt_image'],
) as dag:

   test_email_sent = KubernetesPodOperator(
       image=docker_image,
       cmds=["dbt", "test", "--select", "source:ext_ka.email_sent", "-t", environment ],
       namespace=namespace,
       name="test_email_sent",
       task_id="test_email_sent",
       get_logs=True,
          image_pull_secrets=[k8s.V1LocalObjectReference("artifactory-secrets")],
       dag=dag,
       startup_timeout_seconds=1000,
       )

   execution_mart = KubernetesPodOperator(
       image=docker_image,
       cmds=["dbt", "run", "--select", "mart_email_sent", "-t", environment ],
       namespace=namespace,
       name="execution_mart",
       task_id="execution_mart",
       get_logs=True,
      image_pull_secrets=[k8s.V1LocalObjectReference("artifactory-secrets")],
       dag=dag
       )

   test_mart = KubernetesPodOperator(
       image=docker_image,
       cmds=["dbt", "test", "--select", "mart_email_sent", "-t", environment ],
       namespace=namespace,
       name="test_mart",
       task_id="test_mart",
       get_logs=True,
       image_pull_secrets=[k8s.V1LocalObjectReference("artifactory-secrets")],
       dag=dag
       )

   test_email_sent &amp;gt;&amp;gt; execution_mart &amp;gt;&amp;gt; test_mart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only disadvantage of running dbt in a Kubernetes pod is that you are not able to see the fancy lineage graph of dbt while the steps are executed, as in dbt Cloud. However, the dbt models are generated separately in the Airflow DAG, so you can still see the failing steps and integrate Slack Webhook to receive notifications. Besides, a Github action can be configured to generate dbt docs every time a change is made in the main branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this blog post, we provide a short guide to help you understand the process of migrating eBay Kleinanzeigen's old analytical data pipeline to AWS by leveraging Adevinta's Data Platform.&lt;/p&gt;

&lt;p&gt;Basically, in our new data pipeline, we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;collect events with Kafka topics,&lt;/li&gt;
&lt;li&gt;convert to Delta and store in S3 buckets with a Data Sink,&lt;/li&gt;
&lt;li&gt;create Databricks tables of the stored S3 locations,&lt;/li&gt;
&lt;li&gt;create data models with dbt and store in Databricks (again in an S3 bucket)&lt;/li&gt;
&lt;li&gt;run the dbt models in a schedule with Airflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data marts produced are available in Databricks where analysts can easily access and create a Tableau dashboard. While Databricks is an interface to the analysts, it lacks visibility to the other teams. In the future, we plan to integrate a Glue crawler into our S3 bucket so that we can register the datasets in the data catalogue and achieve integration with the access management services of Adevinta.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, what's next?
&lt;/h2&gt;

&lt;p&gt;Please, keep in mind that Data Mesh is an approach. As such, it may differ based on the company setup. Thanks to the self-service data infrastructure of Adevinta, however, we were able to migrate from Hadoop to AWS in a quite a short period of time with a very small team.&lt;/p&gt;

&lt;p&gt;The next step for our team is that we need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;make the pipeline easy to integrate with all domain teams&lt;/li&gt;
&lt;li&gt;give the teams the necessary technical support&lt;/li&gt;
&lt;li&gt;explain the Data Mesh strategy and the importance of data products&lt;/li&gt;
&lt;li&gt;and make sure the domain teams own the data products by testing, documenting and cataloguing properly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Changes are difficult to make, especially the conceptual ones, but if we want to quickly scale and work with the high volume, diverse and frequently changing data, we will need to do this together.&lt;/p&gt;

</description>
      <category>datamesh</category>
      <category>airflow</category>
      <category>dbt</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Celebrating a Decade of Creativity</title>
      <dc:creator>Kleinanzeigen &amp; mobile.de</dc:creator>
      <pubDate>Tue, 14 Feb 2023 10:33:46 +0000</pubDate>
      <link>https://dev.to/berlin-tech-blog/celebrating-a-decade-of-creativity-337k</link>
      <guid>https://dev.to/berlin-tech-blog/celebrating-a-decade-of-creativity-337k</guid>
      <description>&lt;h2&gt;
  
  
  Over 100 days of collaboration, development &amp;amp; fun
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Recently, we marked a significant milestone in our company’s history: the 10th celebration of our Innovation Days. With over 100 days of teamwork, growth, and enjoyment behind us, we felt it was the perfect moment to share our story with others. Nina Maaß, a talented software engineer and member of the organising team, was kind enough to share insights on how our internal hackathon started and why it continues to be successful.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nina, could you tell us about the origin and motivation behind the founding of the Innovation Days at mobile.de?&lt;/strong&gt;&lt;br&gt;
“Everything started in December 2012. At that time — inspired by a blog post about slack-time* — and under the name of ‘Consumer Slack-Day’ all Product and Tech Teams had the chance once a month to work on the topic they normally don’t have time for. Over the years the format and name changed. With big support from management and organised by highly engaged colleagues, ‘Innovation Days’ became an important part of our Product and Tech culture.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4bh887gspcsjqw1vvavq.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4bh887gspcsjqw1vvavq.JPG" width="800" height="653"&gt;&lt;/a&gt;Nina Maaß while moderating the 10th Innovation Day. © Manuel Krug&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does the format of the Innovation Days look like today?&lt;/strong&gt;&lt;br&gt;
“Currently, the Innovation Days take place on three consecutive days once per quarter. The days are mostly joined by our Product and Tech teams, but also by colleagues from other business units. Over the years not only has the company grown but also the number of participants. Last time around 50 people contributed and over 170 people joined the presentations.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The numbers are impressive — why do you think it’s so successful?&lt;/strong&gt;&lt;br&gt;
“One of the golden rules and basis for its long-term success has always been: ‘When you’re at Innovation Days, you’re at Innovation Days. No daily business except P1 bugs!’ The three days shouldn’t be interrupted by meetings or any other activities.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx47gcehw0l7sthulea4i.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx47gcehw0l7sthulea4i.JPG" width="800" height="533"&gt;&lt;/a&gt;Innovating all day long. © Manuel Krug&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three days sound like a lot of time — how are the days structured?&lt;/strong&gt;&lt;br&gt;
“Innovation Days actually start long before that. We send out invitations up to three weeks in advance. From that moment on, people can add topics with short descriptions into a list of potential projects and can already set up teams.&lt;br&gt;
Then, on the first day in the morning we start with the idea pitches. Every single person or team presents their topic with the request to join. It’s important to know that the pitches are not an ‘automatic’ registration for the final presentation, because ideas can also change or be discarded. The decision whether something will be presented can be decided up to the end of Innovation Days.&lt;br&gt;
For newcomers we offer a brief summary beforehand: ‘Innovation Days in a Nutshell’ explains what all the fuss is about. After the idea pitches, the assembled teams or single contributors start working. What follows is, what we call: Happy Development! Two days of fun bringing your idea to life. In the Pre-Covid time everyone was meeting in the office and worked as long as there was pizza and drinks.&lt;br&gt;
On the third and last day up into the afternoon everyone has still time to work on the projects until the final presentation. The topics needed to be handed in a second time up to 30 minutes before. Every team or single contributor gets the chance to present their results.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feae6i3p3xycrwas87j1a.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feae6i3p3xycrwas87j1a.JPG" width="800" height="533"&gt;&lt;/a&gt;Happy Development! © Manuel Krug&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How would you describe the presentation?&lt;/strong&gt;&lt;br&gt;
“The final presentation is about knowledge sharing and learning. Not only successful projects, but also failures or detours are given room and attention. But of course, Innovation Days are also a challenge. At the end of the final presentation the winners are selected by voting. We’ve determined two categories for outstanding achievements taking into account different team sizes: ‘Winning Team’ for teams with more than three members and ‘Honourable Mention’ for smaller teams or single contributors. With this we want to encourage and reward ideas from everyone.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa4btc7zjazkc5fanxpgi.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa4btc7zjazkc5fanxpgi.JPG" width="800" height="533"&gt;&lt;/a&gt;Getting ready for the final presentation. © Manuel Krug&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If others would like to adopt this concept, could you share some best practices organising an event like this?&lt;/strong&gt;&lt;br&gt;
“Through the years the setup was adapted to the requirements. Currently, we are two people — me and my colleague Daniel Korger. I have been in the orga team since the end of 2019 and Daniel since the beginning of 2020.&lt;br&gt;
We coordinate Innovation Days with management at an early stage in order to avoid conflicts with other company events and of course, daily business. We communicate the event through Slack and — not to underestimate — word of mouth. We send out calendar invitations. We are the moderators for the idea pitches and the final presentation. We accompany the event in all aspects. Our ambition is to constantly improve the event in order to provide the community with the space to work on their projects in an undisturbed and focused way.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcrc533qi8tgmwz6xll8.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcrc533qi8tgmwz6xll8.JPG" width="800" height="533"&gt;&lt;/a&gt;Ajay Bhatia, CEO of mobile.de talks with colleagues during Innovation Days. © Manuel Krug&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You mentioned earlier that Innovation Days used to take place in the office. How is it currently being carried out?&lt;/strong&gt;&lt;br&gt;
“Pre-Covid, Innovation Days were held alternately in one of our offices in Berlin or Dreilinden. However, even then we saw a trend towards remote contribution, so we were well prepared for the ‘home office’ time.&lt;br&gt;
Now Post-Covid, Innovation Days are a remote event, but we start to create more and more of a hybrid experience. That means, who wants to work from the office, is free to do so and many colleagues use this opportunity.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftpbkemai1tngn9o9gaay.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftpbkemai1tngn9o9gaay.JPG" width="800" height="680"&gt;&lt;/a&gt;Hybrid setup of our Innovation Days. © Manuel Krug&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ten years of innovation — there must be a bunch of great ideas. What happened to all those results?&lt;/strong&gt;&lt;br&gt;
“What happens to the projects after Innovation Days is always different and based on the context. The spectrum ranges from exploration of technology or concepts through prototypes or proof of concepts of single features up to tools to improve our daily work. If a project result is promising, the potential is discussed with stakeholders and business owners and prioritised accordingly. And then there are also teams that use the next Innovation Days to continue working on their project in order to improve it or gain new insights.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you remember a project, which made it into the mobile.de platform or the company’s strategy?&lt;/strong&gt;&lt;br&gt;
“Yes, the project ‘Green Mobility’ is now part of our roadmap.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the best thing about Innovation Days?&lt;/strong&gt;&lt;br&gt;
“For me personally, the best thing about the Innovation Days is the community and the support from management. Without this, the event would not be possible and could not have become part of our mobile.de culture. Furthermore, I am always impressed by the results as well as by some colleagues, who jump over their shadows and overcome their fear of talking in front of bigger groups.&lt;br&gt;
And, I will not forget the year, when the tech leadership team tried to participate despite a full calendar, but ended up in front of the whole group to announce: ‘We have failed! We did not stick to our golden rule that when you’re at Innovation Days, you’re at Innovation Days.’ I always remember this, because our Innovation Days are also about sharing learnings and that includes failures.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thank you Nina, for sharing these insights. Would you like to add something for our readers?&lt;/strong&gt;&lt;br&gt;
“I hope we can inspire other companies with our experiences to try out something new. It takes time, patience and iterations to let something like Innovation Days become a permanent part of the culture. Because what works for us doesn’t have to work for others.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fer6ap4hjqdbhpud8cpz9.JPG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fer6ap4hjqdbhpud8cpz9.JPG" width="800" height="533"&gt;&lt;/a&gt;Celebrating ten years of innovation at mobile.de. © Manuel Krug&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The next Innovations Days are taking place now, mid February. We are starting in our 11th year and there are many more to come.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo62e8jv0ey0jbeaaxy6x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo62e8jv0ey0jbeaaxy6x.png" width="800" height="354"&gt;&lt;/a&gt;mobile.de Innovation Days Q1/2023&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Do you have a hackathon or a comparable format like our innovation days in our company or team? Let us know in the comments.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;*Link to blog post about slack-time &lt;a href="https://agiletrail.com/2012/01/09/slack-to-the-rescue-what-you-want-to-do/" rel="noopener noreferrer"&gt;https://agiletrail.com/2012/01/09/slack-to-the-rescue-what-you-want-to-do/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
