<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[The Glovo Tech Blog - Medium]]></title>
        <description><![CDATA[Read about our biggest technical challenges and what it´s like to work at Glovo. https://engineering.glovoapp.com/ - Medium]]></description>
        <link>https://medium.com/glovo-engineering?source=rss----9bbfe5be0af5---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>The Glovo Tech Blog - Medium</title>
            <link>https://medium.com/glovo-engineering?source=rss----9bbfe5be0af5---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 15 May 2026 15:52:24 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/glovo-engineering" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Aurora MySQL at Glovo — The Foundation]]></title>
            <link>https://medium.com/glovo-engineering/aurora-mysql-at-glovo-the-foundation-df1d2ca642a7?source=rss----9bbfe5be0af5---4</link>
            <guid isPermaLink="false">https://medium.com/p/df1d2ca642a7</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[aws-aurora]]></category>
            <category><![CDATA[kubernetes-operator]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[mysql]]></category>
            <dc:creator><![CDATA[Nishaad Ajani]]></dc:creator>
            <pubDate>Mon, 02 Dec 2024 13:46:51 GMT</pubDate>
            <atom:updated>2024-12-02T13:46:51.838Z</atom:updated>
            <content:encoded><![CDATA[<h3>Aurora MySQL at Glovo — The Foundation</h3><p>Let me take you back to a time when managing Aurora MySQL databases at Glovo felt like wrestling with a growing beast. It was 2021, and our small Platform team was juggling a rapidly expanding fleet of databases with tools that, while powerful, were showing their limits. Every new challenge — scaling clusters, rolling out updates, handling upgrades — felt like a mountain we had to climb manually, armed with Terraform, custom scripts and a lot of caffeine.</p><p>We knew there had to be a better way. We dreamed of a system where managing Aurora MySQL clusters didn’t require late-night interventions or painstaking coordination across teams. What if we could build something that just worked — automatically, safely, and at scale? That dream led us down an ambitious path, one where a handful of engineers would build a Kubernetes operator that changed everything.</p><p>This blog series is the story of that journey. It’s about how a small team tackled big problems, transforming database management at Glovo from a tedious manual process into a seamless, automated system. It’s a story of innovation, challenges, and the power of leveraging Kubernetes to not just solve problems but create a foundation for future growth. Join me as we dive into how we built this operator, the impact it had, and what we learned along the way.</p><h3>The Challenge: Growing Pains in a Rapidly Expanding Company</h3><p>Back in early 2021, our database infrastructure at Glovo was manageable — barely. With just a handful of Aurora MySQL clusters, a single Terraform module was enough to keep things running. But as Glovo grew, the cracks in this setup started to show, and what once felt straightforward turned into a maze of complexity.</p><p>It began with <strong>distributed configurations</strong>. Each team owned its own git repository and Terraform workspace, which sounded great for autonomy but quickly turned into a headache. Rolling out a simple update meant tracking down dozens of configurations, hoping nothing broke along the way. It wasn’t long before essential tasks — like scaling, backups, and reboots — became anything but straightforward. These jobs ate up hours of engineering time, and as the number of clusters grew, so did the grind.</p><p>The real pain came with <strong>major version upgrades</strong>. MySQL upgrades are tricky at the best of times, but doing them manually, often late at night to avoid disrupting traffic, was downright brutal. And then there were the inevitable <strong>mishaps</strong> — a misplaced configuration or a poorly reviewed Terraform apply could mean downtime or worse, leaving us scrambling to recover a deleted database cluster.</p><p>As our database fleet ballooned to over 200 clusters, even simple updates became <strong>cumbersome and error-prone</strong>, taking weeks to roll out across all teams. It was clear that the system we’d relied on for so long just wasn’t built to handle this level of growth. We needed a new approach, one that didn’t just patch over the problems but completely rethought how we managed our databases. It was time to scale smarter, not harder.</p><h3>Terraform Scalability Challenges</h3><p>Terraform was our trusty tool for managing infrastructure, but as our needs grew, we started to hit its limits. It’s great for describing the end state you want — “Make it so!” — but not so much for handling the messy in-between. Managing Aurora MySQL clusters highlighted these gaps, especially when we tried to scale.</p><p>Take <strong>complex business logic</strong>, for example. Imagine you need to change the instance type of a database cluster, but only during a specific maintenance window. Terraform doesn’t natively support adding that kind of conditional logic. Either you manually intervene at just the right time or lean on AWS features, like maintenance scheduling, when they’re available. And if they aren’t? You’re stuck with manual effort and a bit of hope.</p><p>Then there were the <strong>orchestration challenges</strong>. For operations like scaling or major version upgrades, we often needed multi-step workflows. A task as simple as resizing an instance might involve draining traffic, updating configurations, restarting instances, and checking everything is back online — steps Terraform can’t sequence dynamically. This left us juggling AWS automation tools where possible and writing custom scripts to fill in the gaps.</p><p>Perhaps the trickiest part was <strong>state management</strong>. Terraform’s state file is great for tracking what’s been done but doesn’t handle transitions well. If changing instance type fails during the scheduled maintenance by AWS, Terraform might think everything’s fine just because the desired state was technically applied. Recovery often meant rolling up our sleeves to manually fix state files — a risky, tedious process.</p><p>It became clear that while Terraform was powerful, it wasn’t designed for the dynamic, time-sensitive workflows that managing Aurora MySQL at scale demanded. We needed something more — something that could handle the transitions, incorporate business logic, and still let us leverage Terraform’s strengths. That’s where our Kubernetes operator came into play.</p><h3>Interim Solutions: Bridging the Gap</h3><p>We knew we couldn’t solve all our challenges overnight, so we introduced several interim measures to ease the growing pains and reduce operational overhead. These stopgap solutions weren’t perfect, but they gave us the breathing room we needed to keep things running as our infrastructure scaled.</p><p>One of the key changes was making better use of existing <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.DBMaintenance.html"><strong>maintenance windows</strong></a> on AWS. Instead of leaving them unmanaged, we optimised how we used maintenance windows, scheduling them during low-traffic hours to reduce risk and improve efficiency. By carefully distributing updates, we minimised the impact of potential issues and ensured non-critical problems could be addressed promptly. This approach wasn’t revolutionary, but it was effective — preserved high availability and provided a reliable, structured way for teams to make certain changes with greater confidence.</p><p>Another improvement was the partial automation of <strong>MySQL version upgrades</strong>. This tool streamlined a notoriously complex process with a structured workflow:</p><ol><li><strong>Clone Creation</strong>: A new clone of the database was provisioned.</li><li><strong>Upgrade Process</strong>: The clone was upgraded to the target MySQL version.</li><li><strong>Binlog Replication</strong>: Synchronisation was maintained between the old and new clusters.</li><li><strong>Integrity Checks</strong>: Data integrity was validated to catch issues early.</li><li><strong>Traffic Cutover</strong>: With manual approval, traffic was shifted to the upgraded cluster.</li></ol><p>These were just two examples. Across the platform, we worked to streamline other operational tasks and build tools that tackled immediate pain points. From automating routine maintenance to refining monitoring and alerting, we made incremental improvements wherever we could.</p><p>While these measures helped reduce some of the toil and risks, they weren’t enough to address the underlying complexity of managing Aurora MySQL at scale. Each solution felt like a patch on a system that needed a complete rethink. We knew the only way forward was to build a more cohesive and automated approach — one that could handle the scale and complexity of our growing infrastructure. That vision set us on the path to creating our Kubernetes operator.</p><h3>The Turning Point: Introducing a Kubernetes Operator</h3><p>The breakthrough came with the decision to build a <a href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/"><strong>Kubernetes operator</strong></a> tailored to manage Aurora MySQL clusters. Kubernetes operators extend the Kubernetes API, encapsulating the logic required to automate the lifecycle of complex applications. This approach aligned perfectly with our goals:</p><h4>Why Operators?</h4><ul><li>Automate complex, application-specific tasks (e.g., scaling, backups, upgrades).</li><li>Manage stateful applications (like databases) seamlessly in Kubernetes environments.</li><li>Provide consistent deployment and management across environments.</li><li>Encapsulate domain-specific knowledge, reducing manual interventions.</li></ul><p>For Glovo, this meant transitioning from manual, distributed workflows to a <strong>centralised and automated control plane</strong>, tailored for Aurora MySQL at scale.</p><h4>The First Generation: A Hybrid Approach</h4><p>In its first iteration, our operator leveraged the <strong>existing Terraform module</strong> instead of building everything from scratch or relying on the AWS RDS operator. This allowed us to capitalise on the rich, business-critical logic already built into our Terraform setup, including:</p><ul><li><strong>Custom Metrics Collectors</strong>: Automated provisioning of Lambda functions to capture detailed InnoDB table and query level metrics that went beyond CloudWatch’s default capabilities.</li><li><strong>MySQL Partitioning rotation: </strong>Lambda functions to automate the creation and rotation of MySQL <a href="https://dev.mysql.com/doc/refman/8.0/en/partitioning-range.html">range partitions</a>, optimising query performance and storage retention for time-series data.</li><li><strong>Disaster Recovery Readiness</strong>: Support for provisioning <strong>Aurora global clusters</strong>, ensuring a robust setup in our disaster recovery (DR) region.</li></ul><p>However, we designed the architecture to clearly separate <strong>developer responsibilities</strong> from <strong>platform management</strong>, ensuring simplicity and safety.</p><h4>Developer-Centric YAML Configuration</h4><p>Developers interacted with the system via a <strong>minimal YAML configuration</strong> stored directly in their <strong>service repositories</strong>. This specification included only the details they cared about, such as instance size, scaling limits, and partitioned tables. For example:</p><pre>apiVersion: storage.platform.glovoapp.com/v1alpha1<br>kind: AuroraResource<br>metadata:<br>  name: orders-db<br>spec:<br>  version: 5.7.mysql_aurora.2.11.2<br>  instanceClass: db.r6g.large<br>  scaling:<br>    targetCpuUsage: 70<br>    minReaders:     1<br>    maxReaders:     3<br>  parameters:<br>    maxConnections: 200<br>  mysqlPartitionedTables:<br>  - name: my_table<br>    intervalType: &quot;DAY&quot;<br>    intervalFormat: &quot;Snowflake&quot;<br>    retention: 7<br>    buffer: 5</pre><p>This approach allowed product teams to define their database requirements declaratively, abstracting away the complexities of underlying infrastructure.</p><h4>Platform-Controlled Terraform Repository</h4><p>On the platform side, all <strong>Terraform code</strong> was centralised in a dedicated repository managed by the Platform team. This repository contained all the Terraform configurations for Aurora MySQL clusters, which were <strong>automatically generated by the Kubernetes operator</strong> based on the developer-provided YAML specifications.</p><p>The repository served as a standardised and centralised home for all database clusters, replacing the fragmented, team-specific Terraform setups that had been manually maintained before. This approach allowed us to:</p><ul><li><strong>Provision and Update Aurora Clusters</strong>: Automatically translate YAML configurations into Terraform code to handle cluster lifecycle tasks.</li><li><strong>Rollout updates faster</strong>: Updates to our custom metrics collectors, and other advanced functionality could be rolled out quickly and transparently from the developer teams.</li><li><strong>Enforce Guardrails</strong>: Use automated checks to validate Terraform plans, ensuring safety and consistency.</li></ul><p>All configurations for database clusters were linked to their own <strong>Terraform Cloud workspace</strong>, creating a controlled environment for running plans and applies. The <strong>lift-and-shift</strong> process brought all existing Terraform configurations under a single, standardised structure, ensuring consistency across all database clusters.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*bU-TLk7rm7cRDo67" /></figure><p>This setup completely eliminated the need for product developers to interact directly with Terraform, reducing errors and freeing them to focus on their applications. Instead, their simple YAML configurations drove the entire process, with the operator handling the generation and application of Terraform code behind the scenes.</p><h4><strong>How It Worked</strong></h4><h4><strong>Developer Workflow:</strong></h4><ol><li>Developers updated their database configurations in a minimal <strong>YAML file</strong> located in their service repositories.</li><li>These changes triggered <strong>GitHub Actions</strong>, which synced the YAML to the corresponding <strong>Kubernetes CRD</strong>.</li><li>The operator then took over, orchestrating the necessary Terraform updates in the platform’s central repository and managing the lifecycle of the database cluster.</li><li>Status updates were reported back to the developer repository via <strong>GitHub commit statuses</strong>, providing visibility into the progress and outcome of the changes.</li></ol><h4>Centralised CI/CD Pipeline:</h4><ul><li>The operator translated the developer’s YAML spec into standardised <strong>Terraform configurations</strong> and committed these to the platform’s central Terraform repository.</li><li>Updates were validated in <strong>Terraform Cloud workspaces</strong>, enforcing safety and consistency:</li><li><strong>Sentinel Checks</strong>: Automatically blocked unsafe changes, such as accidental deletions or misconfigurations.</li><li><strong>Automated PR Validation</strong>: Ensured all changes adhered to predefined standards before being merged and applied.</li></ul><h4>Manual Review When Needed:</h4><p>For high-impact changes — such as major version upgrades, provisioning global clusters, or adjusting disaster recovery setups — the system flagged updates for manual review and approval to ensure additional oversight.</p><h4>Safe Application:</h4><p>Once validated, the operator applied the changes via <strong>Terraform Cloud</strong>, ensuring consistent and safe updates across all environments. Developers could monitor the entire process through the commit status updates in their service repository, ensuring transparency without requiring direct interaction with the Terraform workflows.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rEXnSk7yi1ZrrA5suyjTNw.png" /></figure><h3>Challenges Faced Along the Way</h3><p>Building the Kubernetes operator wasn’t without its hurdles. One particularly tricky challenge arose from how Terraform Cloud interacted with GitHub. As we scaled, we ran into significant bottlenecks caused by <strong>GitHub API rate limits</strong>.</p><h4>Here’s what happened:</h4><ul><li><strong>Rate Limiting on GitHub API</strong>: Terraform Cloud frequently updated Git commit statuses to report the state of each workspace. However, as our fleet of Aurora MySQL clusters grew, these calls overwhelmed the GitHub API, triggering rate limits.</li><li><strong>Unintended Consequences</strong>: When Terraform Cloud hit the rate limit, it couldn’t accurately detect which files had changed. Instead of running plans only for the affected database, Terraform would trigger plans for <strong>all workspaces</strong> in the central repository. This created a cascade of issues:</li><li>The Terraform apply queue became overwhelmed, blocking changes from other teams.</li><li>With limited Terraform Cloud agents, critical updates were delayed, impacting productivity across multiple projects.</li></ul><h4>The Solution: Smarter Commit Status Updates</h4><p>To address this, we made a critical adjustment:</p><ul><li>We updated the operator to enable <strong>commit status updates</strong> in Terraform Cloud workspaces <strong>only on demand</strong>.</li><li>For any database change, the operator dynamically toggled this setting to ensure that only the affected workspace updated Git commit statuses.</li></ul><p>This adjustment drastically reduced the number of API calls to GitHub, avoiding rate limits and ensuring Terraform Cloud only processed the necessary plans. It also prevented the apply queue from being flooded, allowing teams to work without interference.</p><p>This challenge highlighted the complexities of integrating multiple systems at scale, but it also reinforced the value of automation. With this workaround, we ensured our operator could continue to scale alongside Glovo’s growing infrastructure needs.</p><h3>The Results: A Transformed Landscape</h3><p>The introduction of the Kubernetes operator was a game-changer for how we manage Aurora MySQL clusters at Glovo. What started as a small-scale experiment soon became the backbone of our database infrastructure. Here’s what changed:</p><ul><li><strong>Centralised Control</strong>: Gone were the days of fragmented configurations. Now, everything was unified — one consistent approach to managing all clusters across the platform.</li><li><strong>Reduced Toil</strong>: Routine tasks like terraform module updates became automated, giving engineers more time to focus on strategic projects that added value.</li><li><strong>Enhanced Safety</strong>: Built-in guardrails, canary releases of new terraform changes, and automated checks dramatically reduced the risk of human error, ensuring safer deployments and fewer incidents.</li><li><strong>Improved Developer Experience</strong>: With simple YAML files in their service repositories, developers no longer needed to worry about the complexities of Terraform or underlying infrastructure. They could self-service their database needs, boosting productivity and reducing friction.</li></ul><p>This shift didn’t just streamline operations — it reshaped how we think about infrastructure management. The operator turned a complicated, manual process into something that scaled with us, providing reliability and efficiency.</p><h3>Looking Ahead</h3><p>This operator has laid the foundation for a more scalable and efficient infrastructure management system at Glovo. In <strong>Part 2</strong>, we’ll explore how this architecture enabled us to automate one of the most complex and critical tasks: <strong>MySQL version upgrades</strong> — and the advanced features we built to support product teams. Stay tuned!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=df1d2ca642a7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/glovo-engineering/aurora-mysql-at-glovo-the-foundation-df1d2ca642a7">Aurora MySQL at Glovo — The Foundation</a> was originally published in <a href="https://medium.com/glovo-engineering">The Glovo Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Using Airflow in Glovo]]></title>
            <link>https://medium.com/glovo-engineering/using-airflow-in-glovo-6754a2fe79a5?source=rss----9bbfe5be0af5---4</link>
            <guid isPermaLink="false">https://medium.com/p/6754a2fe79a5</guid>
            <category><![CDATA[data]]></category>
            <category><![CDATA[data-orchestration]]></category>
            <category><![CDATA[data-mesh]]></category>
            <category><![CDATA[apache-airflow]]></category>
            <category><![CDATA[glovo]]></category>
            <dc:creator><![CDATA[Pablo Rodríguez Madroño]]></dc:creator>
            <pubDate>Mon, 02 Dec 2024 13:46:29 GMT</pubDate>
            <atom:updated>2024-12-02T13:46:29.516Z</atom:updated>
            <content:encoded><![CDATA[<h3>Using Airflow in Glovo for data orchestration</h3><figure><img alt="Apache Airflow logo." src="https://cdn-images-1.medium.com/max/1024/0*YwROkhgQRCBQXAuW" /><figcaption>Apache Airflow logo</figcaption></figure><h3>Summary</h3><p>In this article we briefly introduce Apache Airflow as a data workflow orchestrator, and we present Glovo’s data strategy based on the Data Mesh paradigm. We then illustrate how Airflow is used in Glovo, and present some customizations that have made a successful implementation of Data Mesh possible. We finally show the evolution towards a declarative approach that truly democratizes data production and usage, making Airflow a cornerstone of Glovo’s data architecture.</p><h3>What is Apache Airflow?</h3><p><a href="https://airflow.apache.org/">Apache Airflow</a> is “an open-source platform for developing, scheduling, and monitoring batch-oriented workflows” [<a href="#1439">1</a>]. It was created by <a href="https://maximebeauchemin.medium.com/">Maxime Beauchemin</a> in 2014 while working at Airbnb to handle increasingly complicated data engineering pipelines. The project joined the <a href="https://www.apache.org/">Apache Software Foundation</a> in 2016, and became a top-level project in 2019, ensuring its future continuity [<a href="#a4a0">2</a>]. Today, it’s probably the most used orchestration tool in the Data Engineering field [<a href="#03d9">3</a>][<a href="#062a">4</a>].</p><p>Orchestration, in the context of Data Engineering, automates the <strong>scheduling of jobs</strong>, and the <strong>sequencing of the steps</strong> required to perform the movement and transformation of data between systems. It is crucial to ensure timeliness and quality of the data to be used in analytics, reporting, modeling or machine learning [<a href="#8b69">5</a>][<a href="#b0b7">6</a>][<a href="#d97c">7</a>].</p><figure><img alt="Sarah Ioannides conducting an orchestra." src="https://cdn-images-1.medium.com/max/620/0*bwivaPefpcbluMpw" /><figcaption>Sarah Ioannides conducting an orchestra. Credit: Izabel.zambrzycki, <a href="https://creativecommons.org/licenses/by-sa/4.0">CC BY-SA 4.0</a>, via <a href="https://commons.wikimedia.org/wiki/File:Sarah_Ioannides-_Conducting.jpg">Wikimedia Commons</a></figcaption></figure><p>Airflow organizes the data pipelines or workflows in so-called <a href="https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/dags.html">DAGs</a> (Directed Acyclic Graphs):</p><ul><li>The different jobs to be performed are represented as nodes in a graph, which are called <strong>tasks</strong> in Airflow. Tasks are instances of <strong>operators</strong> that perform a certain type of work (for example, reading from a database or writing to a file).</li><li>The relationships between the tasks are reflected as arcs connecting the nodes, and they are called <strong>dependencies</strong> in Airflow. These relationships between tasks are directed: a certain task needs to be executed after one or more other tasks.</li><li>Being acyclic means that once a task is completed it is not possible to go back and re-execute it.</li></ul><p>DAGs are not exclusive to Airflow, and there are <a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph">many applications</a> of this data structure.</p><figure><img alt="Example of a DAG." src="https://cdn-images-1.medium.com/max/480/1*azAz7BrFAyFgA9vJTqiIiQ.png" /><figcaption>Example of a DAG</figcaption></figure><p>These properties allow Airflow to implement the “sequencing of the steps” component of orchestration by ensuring that:</p><ul><li>There is a clear beginning of the execution of the tasks.</li><li>There is a clear path forward when each task is completed.</li><li>There is a clear ending of the execution of the tasks.</li><li>Eventually all the tasks will be completed.</li></ul><p>Additionally, Airflow implements the “scheduling of jobs” component of orchestration by allowing a <a href="https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/cron.html">cron-like expression</a> in the DAGs, and determining at which moment in time a DAG needs to be run. More complicated running configurations can be set through <a href="https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/timetable.html">timetables</a>, which do not need to be periodic in time.</p><p>Dependencies between DAGs can be handled in several ways:</p><ul><li>The dependent DAG’s schedule could be set up so that it starts after the dependencies have normally completed execution. However, this setup is vulnerable to errors or delays, as there is no way to verify whether the dependencies have effectively run.</li><li>It is also possible to trigger a DAG run from another DAG through the <a href="https://airflow.apache.org/docs/apache-airflow/stable/_api/airflow/operators/trigger_dagrun/index.html">TriggerDagRun operator</a>.</li><li>Airflow’s recommended way is to use the <a href="https://airflow.apache.org/docs/apache-airflow/stable/howto/operator/external_task_sensor.html">ExternalTaskSensor operator</a> in the dependent DAG to check for the dependencies to be completed.</li></ul><h3>Glovo’s data strategy</h3><p>Three years ago, data at Glovo was in distress. The growth of the business implied also a growth in the usage of data for informed decision-making, but the infrastructure and the organization supporting the increased usage could not scale more. There was no way to make our centralized Data Warehouse larger or more powerful. Outages were frequent, recovery times were in the order of days, and there was no clear technical, operational or business owner for many of the data processes.</p><p>Glovo decided to <a href="https://medium.com/glovo-engineering/data-mesh-its-not-just-about-tech-it-s-about-ownership-and-communication-b8f70a61affe">migrate</a> this centralized approach to a <a href="https://martinfowler.com/articles/data-monolith-to-mesh.html">Data Mesh organization</a>. After more than two years of work, the decentralization strategy has been a resounding success, and we have been able to turn off our gigantic Data Warehouse. The four pillars of Data Mesh have been crucial to achieve this level of success [<a href="#3f3c">8</a>]:</p><figure><img alt="Logical architecture of the Data Mesh approach, showing the four pillars, from https://martinfowler.com/articles/data-mesh-principles.html." src="https://cdn-images-1.medium.com/max/1000/0*xHoAFybkAZSGojWA" /><figcaption>Logical architecture of the Data Mesh approach, showing the four pillars, from <a href="https://martinfowler.com/articles/data-mesh-principles.html">https://martinfowler.com/articles/data-mesh-principles.html</a></figcaption></figure><h4>Domain ownership</h4><p>In the same way as Engineering teams have achieved decentralization by embracing <a href="https://www.domainlanguage.com/ddd/">domain-driven design</a> and adopting the <a href="https://www.agileanalytics.cloud/blog/team-topologies-the-reverse-conway-manoeuvre">Reverse Conway Maneuver</a>, Data teams need to be arranged into separate business domains. Each domain covers a <a href="https://martinfowler.com/articles/data-monolith-to-mesh.html#ArchitecturalFailureModes">bounded context</a>, for which the data team has full ownership and is fully accountable. Each domain data team decides which data to expose to other domains, while keeping the implementation details internally.</p><h4>Data as a product</h4><p>In Data Mesh, a data product is the smallest architectural block that can be deployed as a cohesive unit, and is the result of applying product thinking to domain-oriented data. It is composed of the code needed to perform the required transformations, the data resulting from those transformations, the metadata that identifies the product and the outputs, and the infrastructure required to run the previous elements.</p><p>A data product in Glovo represents a set of tables designed to fulfill the same use cases / user needs, with the same timeliness, loading frequency and criticality requirements. These tables can be exposed to users via multiple interfaces such as other data products, query engines, BI tools or others. There is no way to produce a data table that is outside of the data product enclosure.</p><h4>Self-serve data platform</h4><p>Data domain teams can autonomously own their data products by having access to a data platform that provides a higher level of abstraction than the direct management level. This platform removes the complexity and friction of provisioning and managing the lifecycle of data products by providing simple declarative interfaces, and implementing the cross-cutting concerns that are defined as a set of standards and global conventions across the organization. The self-serve data platform also includes capabilities to lower the cost and specialization needed to build data products.</p><h4>Federated computational governance</h4><p>Data Mesh follows a distributed system architecture: a collection of separate data products, each with independent lifecycles, built and deployed by autonomous data domain teams. However, to get greater value, these independent data products need to interoperate. which is possible through a governance model that embraces decentralization and domain self-sovereignty, global standardization, a dynamic topology, and, most importantly, automated execution of decisions by the data platform.</p><h3>Usage of Airflow in Glovo</h3><p>Airflow honors its orchestrator role by acting as the central piece in the computation of Data Products in Glovo, as illustrated below:</p><figure><img alt="Airflow as an orchestrator for Data Products." src="https://cdn-images-1.medium.com/max/1024/1*__AJ2f1grl43TmxyrPG7tA.png" /><figcaption>Airflow as an orchestrator for Data Products</figcaption></figure><p>Each Data Product unit includes at least one Airflow DAG for periodic computation, although in many cases there are additional DAGs for a variety of purposes:</p><ul><li>Running some transformations that are different from the main ones: they have a different periodicity or temporality, a different intent or even for splitting outputs with different criticality.</li><li>Performing initial data loads.</li><li>Backfilling data.</li><li>Doing auxiliary operations such as table definition changes or deletions.</li></ul><p>Regardless of their purpose, all the Data Product DAGs have the same general structure, although some of the blocks may not always be present:</p><figure><img alt="General structure of a DAG in Glovo." src="https://cdn-images-1.medium.com/max/977/0*9Myhmm8aPFwGsvs-" /><figcaption>General structure of a DAG in Glovo</figcaption></figure><h4>DagFactory to simplify DAG creation</h4><p>This general structure has led us to build an internal package to abstract and simplify the definition of DAGs. Writing convoluted Python code defining a workflow is replaced by a much simpler file setting up a DAG configuration. We call this module <strong>DagFactory</strong>, much inspired by the dag-factory package by Astronomer [<a href="#3eb3">9</a>] and, to a lesser degree, by the airflow-declarative project [<a href="#6064">10</a>].</p><p>Below there is an example of how a DAG is defined in DagFactory:</p><pre>from datetime import datetime<br>from datetime import timedelta<br>from pathlib import Path<br><br><br>from data_pipeline_tools.airflow.dag_factory.dag_factory import DagFactory<br><br><br>dag_configuration = {<br>   &quot;dag_name&quot;: &quot;my_first_dag_factory_dag&quot;,<br>   &quot;image_version&quot;: &quot;0.2.22&quot;,<br>   &quot;dags_path&quot;: str(Path(__file__).parent.resolve()),<br>   &quot;process_name&quot;: &quot;calculate_odps_courier_distances&quot;,<br>   &quot;domain&quot;: &quot;courier&quot;,<br>   &quot;data_product_name&quot;: &quot;order_flow&quot;,<br>   &quot;data_product_key_prefix&quot;: &quot;OF&quot;,<br>   &quot;schedule_interval&quot;: &quot;30 5 * * *&quot;,<br>   &quot;default_args&quot;: {<br>       &quot;owner&quot;: &quot;Operations Data Engineering&quot;,<br>       &quot;description&quot;: &quot;A set of ODPs covering order-level and city-day KPIs related to courier distances of Orders.&quot;,<br>       &quot;start_date&quot;: datetime(2021, 11, 12),<br>       &quot;retries&quot;: 2,<br>       &quot;email_on_failure&quot;: False,<br>       &quot;email_on_retry&quot;: False,<br>       &quot;retry_delay&quot;: timedelta(minutes=5),<br>       &quot;depends_on_past&quot;: False,<br>       &quot;max_active_runs_per_dag&quot;: 1,<br>   },<br>   &quot;runtime_date_local&quot;: &quot;&#39;2022-01-08&#39;&quot;,<br>   &quot;runtime_date_dev&quot;: &quot;&#39;2022-04-15&#39;&quot;,<br>   &quot;num_days_local&quot;: 8,<br>   &quot;num_days_default&quot;: 30,<br>   &quot;slack_channel_prod&quot;: &quot;data-mesh-monitors-courier&quot;,<br>   &quot;slack_channel_dev&quot;: &quot;data-mesh-monitors-courier-dev&quot;,<br>   &quot;slack_conn_id&quot;: &quot;slack_webhook&quot;,<br>   &quot;script_module&quot;: &quot;order_flow.jobs.courier_order_flow_job&quot;,<br>   &quot;jobs&quot;: {<br>       &quot;courier_distances_points_intermediate&quot;: [],<br>       &quot;courier_distances_order_level_attributes&quot;: [<br>           &quot;courier_distances_points_intermediate&quot;<br>       ],<br>   },<br>   &quot;sensor_specs&quot;: {<br>       &quot;ORDER_DESCRIPTORS_ORDER_DESCRIPTORS&quot;: {<br>           &quot;checkpointer_path&quot;: &quot;COD__DAG_CHECKPOINT_PATH&quot;,<br>           &quot;domain&quot;: &quot;central&quot;,<br>           &quot;product&quot;: &quot;central_order_descriptors&quot;,<br>           &quot;task&quot;: &quot;order_descriptors&quot;,<br>           &quot;freshness_hours&quot;: 8,<br>           &quot;timeout_hours&quot;: 8,<br>           &quot;mode&quot;: &quot;reschedule&quot;,<br>           &quot;poke_interval&quot;: 300,<br>       },<br>   },<br>}<br><br><br># Magic words, DO NOT MODIFY<br>airflow_dag_factory = DagFactory(**dag_configuration)<br>globals()[dag_configuration[&quot;dag_name&quot;]] = airflow_dag_factory.create_pyspark_dag()</pre><p>Although this seems quite a simple DAG, in reality it is formed by 9 tasks (not counting groups). There is a stark difference with the code requirements of a standard Airflow DAG definition: the DagFactory script is much shorter, reducing the cognitive load required to understand the structure, and lowering the possibility of introducing errors.</p><p>As a parallel benefit, this package has brought a high level of standardization in the definition and operation of the Data Products. Before DagFactory, the DAGs defined by the different domains, and even the ones in the same domain, had significant differences in the grouping of tasks, naming, parameterisation, and others. This made operating the DAGs quite dangerous, as mistakes were relatively easy to make, even leading to accidental deletion of data. After implementing DagFactory, all the workflows behave in the same way, and anyone can operate them with confidence that no unexpected side effects will occur.</p><p>Another benefit of the standardization is that the blocks of tasks forming the general structure of the DAGs have the same name across all of the data pipelines. In particular, the block of transformations always ends with a “transformations_end” task. This has been crucial to facilitate the creation of early alerts for DAG failures in observability tools using only <a href="https://airflow.apache.org/docs/apache-airflow/stable/administration-and-deployment/logging-monitoring/metrics.html#metric-descriptions">standard Airflow metrics</a>.</p><figure><img alt="The “transformations_end” task is always present at the end of the transformations group in the general structure the DAGs." src="https://cdn-images-1.medium.com/max/1024/0*D-CypZJ7_i25Tm3K" /><figcaption>The “transformations_end” task is always present at the end of the transformations group in the general structure the DAGs</figcaption></figure><p>In summary, DagFactory has been an accelerator to the Data Engineers tasked with creating Data Products.</p><h4>Checkpoints and sensors to manage DAG dependencies</h4><p>Another component that has been developed in Glovo is an alternative way to ensure that the data dependencies for the transformations contained in a DAG are met before starting to process them:</p><ul><li>In every DAG, a custom operator writes a small JSON file indicating the time it has executed. We call these files checkpoints, and the operator is named CheckpointerOperator.</li><li>Each dependent DAG can use custom <a href="https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/sensors.html">sensor</a> operators that understand the previous file format, and are able to determine whether the execution can take place or it should be kept on hold while the dependencies finish. We call this sensor operator CheckpointSensor.</li></ul><pre>{<br>  &quot;data&quot;:{<br>    &quot;updated_at&quot;: &quot;2024-09-24T08:07:51.058120+00:00&quot;,<br>    &quot;backfilling&quot;: false<br>  }<br>}</pre><p>These custom checkpoints allow greater flexibility than the standard solutions, as they abstract out the Data Product contents from the DAG that generates them. That is, they work at the table level, and frees the owners of a Data Product to define how it is computed without affecting their consumers. In consequence, they favor the separation in domains that is key to our data strategy.</p><p>In the general structure of a DAG we saw how the sensors are the first set of tasks to be run. As for the checkpoints, they are created as part of the transformations group of tasks. This group is formed by chaining together different transformation blocks, each of them composed of three stages:</p><ul><li>The computation of the transformation, either a <a href="https://spark.apache.org/docs/latest/api/python/index.html">PySpark</a> step or a set of <a href="https://www.getdbt.com/">dbt</a> models.</li><li>A data quality assessment of the transformed data.</li><li>The creation of a checkpoint file.</li></ul><p>If the computation of the transformation or the data quality assessment tasks fail, then the checkpoint is not generated, and downstream users are not signaled that a particular Data Product output is ready for consumption.</p><figure><img alt="The three steps of a data transformation block." src="https://cdn-images-1.medium.com/max/505/0*nUrMhy4tLelq4Eka" /><figcaption>The three steps of a data transformation block</figcaption></figure><p>Different 3-step transformation blocks can be linked according to their dependencies so that the overall process is performed in the right order. Also, transformations computing more than a single output can be split in parallel blocks to allow a more granular control of the checkpoints. In this case, some transformation blocks may have failed, but checkpoints would be generated for the successful blocks. This pattern increases the robustness of the Data Mesh, as subsequent Data Product DAGs dependent on the successful outputs can start their updates.</p><figure><img alt="Example of chained transformation blocks to account for dependencies." src="https://cdn-images-1.medium.com/max/787/0*WNsCyqeWt1Hh9XC9" /><figcaption>Example of chained transformation blocks to account for dependencies</figcaption></figure><figure><img alt="Example of split transformation blocks to increase robustness." src="https://cdn-images-1.medium.com/max/460/0*v78Gdcbyf3UQFO5C" /><figcaption>Example of split transformation blocks to increase robustness</figcaption></figure><p>Checkpoints along with transformation splitting have increased the overall availability of data, which would be seriously impaired if checkpoints worked only at the Data Product level.</p><h3>The next iteration: more democratization and autonomy</h3><p>The first implementation of Data Mesh has been so successful that Glovo has developed an even simpler approach based on purely declarative interfaces, as was publicly introduced in our <a href="https://www.meetup.com/es-ES/barcelona-tech-talks/events/298482872/">Data Experts’ RoundTable Meetup</a> some months ago. The main advantage of this approach is a higher abstraction layer over the way of defining data transformations and the underlying infrastructure that runs them. As a consequence, the technical skills and the cognitive load needed to build a data product are highly reduced. This has produced a powerful democratization of data, and a surge in the value of the Data Mesh (which is based on the number of meaningful relationships among data products, not on the number of data products <em>per se </em>[<a href="#ef1a">11</a>]).</p><p>The comparison below shows the differences between the first and the second approaches to Data Mesh:</p><ul><li>In Data Mesh v1 the <em>data product creators</em> were only Data Engineers, whereas in the Declarative Data Mesh they can be anyone with SQL and basic coding skills.</li><li>In Data Mesh v1 the <em>infrastructure management </em>was distributed and handled by each data domain, whereas in the Declarative Data Mesh it belongs to a centralized data platform.</li><li>In Data Mesh v1 the <em>computing engine availability </em>was limited and created on demand, whereas in the Declarative Data Mesh it is always on.</li><li>In Data Mesh v1 the <em>orchestration </em>was managed by each domain, whereas in the Declarative Data Mesh it belongs to a centralized data platform.</li><li>In Data Mesh v1 there was no <em>golden path and standardized structure</em>, whereas in the Declarative Data Mesh it is well-defined and enforced.</li><li>In Data Mesh v1 the <em>maintainability </em>was low due to the lack of standardized structure, whereas in the Declarative Data Mesh it is high due to the centralization of infrastructure and orchestration.</li><li>In Data Mesh v1 the <em>cost efficiency </em>was low and managed by each domain, whereas in the Declarative Data Mesh it is high and centrally managed.</li><li>In Data Mesh v1 the <em>technical complexity </em>and the <em>testability </em>were high, whereas in the Declarative Data Mesh they are low.</li><li>In Data Mesh v1 the <em>time to develop a data product </em>was in the order of hours to weeks, whereas in the Declarative Data Mesh it is in the order or minutes to hours.</li></ul><p>Airflow plays a crucial role in Glovo’s data platform. Each declarative Data Product is mapped to a DAG in a centralized Airflow instance, which is one of the main visible interfaces of the data platform (the other being the query/computing engine). Owners have full capacity to operate the DAGs of their data products: clear tasks or DAG runs, marking them as successes or failures, trigger manual runs, and enable or disable entire DAGs. This capacity goes in line with the “domain ownership” and the “self-serve data platform” principles of Data Mesh: owners cannot ensure timeliness and quality of the data they are responsible for if they are not able to operate their data products effectively. <a href="https://www.goodreads.com/quotes/8630521-with-great-power-comes-great-responsibility-it-is-true-but">A great responsibility needs to bring great power along</a>.</p><h4>Declarative DAG definition</h4><p>In the new approach, DAGs are automatically created from a declarative definition of the tasks to be executed. Only an internal Python package defining a SDK for Data Product creation is required to start building pipelines, as illustrated in the following code fragment:</p><pre>from glovo_data_platform.declarative.manager import DataProductManager<br>from glovo_data_platform.declarative.utils import print_deployment_info<br><br><br>def data_product_definition() -&gt; DataProductManager:<br>   data_product_manager = DataProductManager(<br>       domain=&quot;growth&quot;,<br>       name=&quot;sample_ddp_scripting&quot;,<br>       owner=&quot;pablo.rodriguez@glovoapp.com&quot;,<br>       tier=&quot;t2&quot;,<br>       contacts=[<br>           {&quot;kind&quot;: &quot;email&quot;, &quot;value&quot;: &quot;ga.eng@glovoapp.com&quot;},<br>           {&quot;kind&quot;: &quot;email&quot;, &quot;value&quot;: &quot;pablo.rodriguez@glovoapp.com&quot;},<br>       ],<br>   )<br><br><br>   data_product_manager.add_sql_transformation(<br>       data_classification=&quot;l0&quot;,<br>       sql=&quot;&quot;&quot;SELECT<br>               gsc_date,<br>               COUNT(1) as cnt_records<br>           FROM<br>               &quot;delta&quot;.&quot;growth_master_attribution_odp&quot;.&quot;google_search_console&quot;<br>           GROUP BY gsc_date&quot;&quot;&quot;,<br>       partition_by=[],<br>       target_table=&quot;summary_gsc&quot;,<br>       write_mode=&quot;FULL&quot;,<br>       is_odp=True,<br>   )<br>   return data_product_manager<br><br><br>if __name__ == &quot;__main__&quot;:<br>   schedule = None<br>   publish = False<br>   creation_reason = &quot;Testing creation of T2 DDPs through scripting.&quot;<br>   revision_name = None<br>   data_product_manager = data_product_definition()<br>   revision = data_product_manager.submit(<br>       schedule=schedule,<br>       publish=publish,<br>       creation_reason=creation_reason,<br>       revision_name=revision_name,<br>   )<br>   print_deployment_info(revision)</pre><p>The path between this code and an Airflow DAG is not straightforward, however, although this is hidden from the Data Product creator. The SDK first encodes all the definitions in a JSON DTO (Data Transfer Object). Secondly, the SDK invokes an internal API to deploy the Data Product. Finally, the SDK communicates the result of the deployment to the Data Product developer.</p><p>The internal API is in reality the gateway to the <strong>Meshub</strong> system. Meshub stores the defining characteristics of the Data Products, manages their lifecycle, and provides information for Data Mesh Governance purposes, among several other functions. When deploying a Data Product, the appropriate lifecycle management methods of Meshub validate the encoded Data Product definition, attach the relevant parameters of the different Airflow operators to be used, and copy the modified JSON DTO into a Python file ready for Airflow to parse as a DAG. The following code block shows the file generated from the sample declarative Data Product code illustrated above.</p><pre>from glovo_data_platform.declarative_airflow.dag_creator import build_dags<br><br>REVISION_DEPLOY_SPEC_JSON = r&quot;&quot;&quot;<br>{<br>  &quot;revision&quot;:{<br>    &quot;revision_id&quot;:&quot;2af464bc-96f5-4443-b73f-f0507a61fdde&quot;,<br>    &quot;data_product&quot;:{<br>      &quot;domain&quot;:&quot;growth&quot;,<br>      &quot;name&quot;:&quot;sample_ddp_scripting&quot;,<br>...<br>}<br>&quot;&quot;&quot;<br><br>globals().update(build_dags(REVISION_DEPLOY_SPEC_JSON, __file__))</pre><p>The conversion of the file to an actual Airflow DAG is performed by a custom package at every <a href="https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/dagfile-processing.html">file processing</a> cycle. This step translates the JSON DTO into Airflow operators, relationships, and parameters. Additional operators for execution control are also added. The following DAG is the result of processing the Python file shown above:</p><figure><img alt="Airflow DAG for a simple declarative Data Product." src="https://cdn-images-1.medium.com/max/720/0*g1KWpGdrLcBUGBzg" /><figcaption>Airflow DAG for a simple declarative Data Product</figcaption></figure><p>The whole process is summarized in the next diagram:</p><figure><img alt="Steps to deploy a declarative definition of a Data Product as an Airflow DAG." src="https://cdn-images-1.medium.com/max/200/1*12KVjMq8aTr0y6ga_N3SpQ.png" /><figcaption>Steps to deploy a declarative definition of a Data Product as an Airflow DAG</figcaption></figure><h4>Checkpoints and sensors to manage DAG dependencies</h4><p>The way to handle DAG dependencies has evolved towards a more orchestrator-independent solution, in exchange for more cloud-dependent components. In particular, the checkpointing subsystem leverages several services from AWS, Glovo’s cloud provider. Equivalent components can be found in other cloud providers.</p><p>The following diagram shows this process schematically:</p><figure><img alt="Architecture of declarative checkpoints." src="https://cdn-images-1.medium.com/max/200/1*X5JdPZZE3wQJ2CuL2SwMUQ.png" /><figcaption>Architecture of declarative checkpoints</figcaption></figure><p>As Data Product outputs are ultimately daily-partitioned Delta Lake files written in AWS S3, the checkpointing system is notified whenever a Delta changelog file is added. This triggers a lambda function that processes the changelog and extracts the paths of the modified partitions. These paths are then checked in Glue to get the database and the table names. Finally, the information about which table and partitions have been modified is recorded in a DynamoDB table for later usage.</p><figure><img alt="Contents of the DynamoDB table for checkpoints." src="https://cdn-images-1.medium.com/max/1024/0*z-pafnWz6p924Bfv" /><figcaption>Contents of the DynamoDB table for checkpoints</figcaption></figure><p>Downstream Data Products can check whether their dependencies have completed their processes through a custom CheckpointSensor operator:</p><figure><img alt="Transformation task with a sensor checking a dependency." src="https://cdn-images-1.medium.com/max/499/0*QiZ-JFXMuP_aA0VA" /><figcaption>Transformation task with a sensor checking a dependency</figcaption></figure><p>The custom CheckpointSensor operator queries the DynamoDB for the existence of a partition of a particular table. An interplay between <a href="https://airflow.apache.org/docs/apache-airflow/stable/templates-ref.html">Airflow macros</a> and partition names allow checking whether the daily data required for a given execution is ready or not:</p><pre>wait_for_order_descriptors_v2 = data_product_manager.add_wait_for_table(<br>    domain=&quot;central&quot;,<br>    data_product=&quot;order_descriptors&quot;,<br>    table=&quot;order_descriptors_v2&quot;,<br>    partitions=[&quot;p_creation_date={{ data_interval_start | ds }}&quot;],<br>)</pre><h3>Conclusion: the role of Airflow in Glovo’s Data Mesh</h3><p>Whether in the first implementation of Data Mesh or in the declarative approach, Airflow is a cornerstone in Glovo’s data architecture. Going beyond the already powerful features of Airflow, Glovo has implemented improved components to handle dependencies between the DAGs that orchestrate the computation of Data Products. Also, abstractions to simplify the definition of DAGs have been designed in order to reduce the cognitive load to build Data Products.</p><p>Airflow is the main interface to inspect and operate the computation of most of the Data Products that compose Glovo’s Data Mesh. Understanding how Airflow works is crucial, as anyone with basic coding skills is now able to create Data Products, bringing a true democratization of data transformation and usage across the company.</p><h3>References</h3><p>[1] <a href="#54b6">^</a> <a href="https://airflow.apache.org/docs/apache-airflow/stable/index.html">https://airflow.apache.org/docs/apache-airflow/stable/index.html</a></p><p>[2] <a href="#54b6">^</a> <a href="https://airflow.apache.org/docs/apache-airflow/stable/project.html#history">https://airflow.apache.org/docs/apache-airflow/stable/project.html#history</a></p><p>[3] <a href="#54b6">^</a> <a href="https://gradientflow.com/wp-content/uploads/2022/06/GradientFlow-2022-Workflow-Orchestration-Report.pdf">https://gradientflow.com/wp-content/uploads/2022/06/GradientFlow-2022-Workflow-Orchestration-Report.pdf</a></p><p>[4] <a href="#54b6">^</a> <a href="https://6sense.com/tech/workflow-automation">https://6sense.com/tech/workflow-automation</a></p><p>[5] <a href="#c952">^</a> <a href="https://en.wikipedia.org/wiki/Orchestration_(computing)">https://en.wikipedia.org/wiki/Orchestration_(computing)</a></p><p>[6] <a href="#c952">^</a> <a href="https://www.reddit.com/r/dataengineering/comments/uvckp1/can_someone_please_explain_orchestration_and_why/">https://www.reddit.com/r/dataengineering/comments/uvckp1/can_someone_please_explain_orchestration_and_why/</a></p><p>[7] <a href="#c952">^</a> <a href="https://www.ascend.io/blog/what-is-data-pipeline-orchestration-and-why-you-need-it/">https://www.ascend.io/blog/what-is-data-pipeline-orchestration-and-why-you-need-it/</a></p><p>[8] <a href="#f3a0">^</a> <a href="https://martinfowler.com/articles/data-mesh-principles.html">https://martinfowler.com/articles/data-mesh-principles.html</a></p><p>[9] <a href="#e9d9">^</a> <a href="https://github.com/astronomer/dag-factory">https://github.com/astronomer/dag-factory</a></p><p>[10] <a href="#e9d9">^</a> <a href="https://github.com/rambler-digital-solutions/airflow-declarative">https://github.com/rambler-digital-solutions/airflow-declarative</a></p><p>[11] <a href="#f961">^</a> Dehghani, Zhamak. Data Mesh: Delivering data-driven value at scale. O’Reilly Media, Inc. March 2022. ISBN: 9781492092391.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6754a2fe79a5" width="1" height="1" alt=""><hr><p><a href="https://medium.com/glovo-engineering/using-airflow-in-glovo-6754a2fe79a5">Using Airflow in Glovo</a> was originally published in <a href="https://medium.com/glovo-engineering">The Glovo Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How We Engineered a Scalable Architecture to Power Videos, Social, and Picks in Our Delivery App]]></title>
            <link>https://medium.com/glovo-engineering/how-we-engineered-a-scalable-architecture-to-power-videos-social-and-picks-in-our-delivery-app-1e0b7f7dfdca?source=rss----9bbfe5be0af5---4</link>
            <guid isPermaLink="false">https://medium.com/p/1e0b7f7dfdca</guid>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[innovation]]></category>
            <dc:creator><![CDATA[Glovo Technology]]></dc:creator>
            <pubDate>Fri, 29 Nov 2024 10:32:56 GMT</pubDate>
            <atom:updated>2024-11-29T10:28:32.114Z</atom:updated>
            <content:encoded><![CDATA[<p>One month ago, we launched three new features in Glovo: Videos, Social, and Picks. These features enable users to experience a more engaging, social, and personalized browsing experience. We aimed to introduce new ways for users to explore, connect, and save their favorite stores with minimal friction. The main target of these features was to introduce them in the Store Wall, a screen within our app where users can see a list of stores or products for a specific category.</p><p>We wanted to deliver these features within a strict 90-day timeline, and it was possible because we have an architecture capable of handling high modularity, scalability, and rapid iteration. This post explores the architecture we built a year ago — powered by a plugin-based, server-driven design with a Backend-for-Frontend (BFF) service layer. We will explain how this approach allowed more than ten teams to work in parallel, coordinate complex dependencies, and maintain stability for a seamless user experience.</p><h3>Engineering Videos, Social, and Picks</h3><h4>1. Videos: Enhancing User Engagement with Dynamic Content</h4><p>Adding Videos to the Store Wall meant handling high-resolution media while maintaining performance. The goal was to add a new way of discovering products through videos and to achieve this, a new video carousel was added to the Store Wall.</p><p>This new feature delegates the domain logic to a specific microservice in charge of customer content discovery. This service encapsulates the logic for calling multiple microservices across different domains to retrieve videos available at the user’s location, filtering to keep only those related to available products, and enriching them with relevant information.</p><h4>2. Social: Integrating Friend Recommendations and Social Proof</h4><p>The Social feature leverages user networks by recommending products your friends often order. This involved connecting data streams from social profiles, orders, and product ratings.</p><p>Like Videos, this feature delegates the domain logic to a specific service in charge of customer content discovery, encapsulating all the logic of getting the product recommendations (provided by Data models), filtering by store and product availability, and enriching with all information.</p><h4>3. Picks: A Modular Approach to Personalized Curation</h4><p>A <em>pick</em> is a list of stores that the customer decides to group. It is similar to creating a playlist in a music app. Picks allow users to organize their favorite stores, adding personalization to the Store Wall. This introduced specific requirements, such as modularity for different types of stores and future support for sharing and social integration.</p><p>In the Store Wall, we provide quick and easy access to the user’s picks and favorites. For this, the Store Wall delegates to the Picks microservice the logic of getting the users’ picks, filtering by store availability, and enriching with relevant information.</p><h3>Evolution of the Store Wall with a Plugin Architecture and Server-Driven UIs: Building for Flexibility and Scalability</h3><p>We redesigned the Store Wall screen a year ago to use an architecture that enables dynamic and personalized store walls powered by templates. We needed to deliver high-performing, category-specific experiences without overloading the client application. Here’s how we structured it:</p><ul><li><strong>Templates</strong>: We call Templates a group of components/features (internally known as Modules) inside a screen, with their specific positions. You can imagine this as how the visual components on your screen will be presented. Those components or visual elements come both from a configuration, enabling business to inject special features, and some others come from Machine Learning (ML) models targeting a better experience and adoption of a user. <br>Technically speaking, each Template provides a list of independent modules that we will execute and show configuration details for rendering a specific Store Wall. For instance, the “Food” category has a different layout from “Retail,” with each view receiving specific modules in pre-defined positions.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/312/0*-CB6WCPRw2rkk0vf" /></figure><ul><li><strong>Template Selection</strong>: We allow selecting different templates for the store wall screen based on category, country, city, user segment, and even by specific dates, which is usually helpful for events like Sant Valentine’s or Halloween. We also allow experimentation to test different templates for the same category to define the best alternative. As an example, here we have a template for a restaurant category in Spain where each selected module is marked with a red square.</li><li><strong>Orchestrator</strong>: The Orchestrator is the central engine executor, controlling the process of building all modules. This engine receives a Template as an input, together with a RequestContext, which has the current request details like user location and device information, for example. With these, it is in charge of calling every module that needs to be executed. It is also in charge of reporting metrics for each module (success, failures, and latencies), but it also throws errors in case a critical module fails. Lastly, it is also responsible for ensuring that each module is resolved within its latency thresholds and that the whole template is generated before its timeout for that screen. This allows us to:<br> — Prevent failures and ensure seamless experience: If a module considered not critical fails, it is ignored, returning all the other content to the user. The same happens if the module is taking more time than expected to be executed. This also prevents teams collaborating in the Store Wall from breaking it if a bug or unexpected behavior is introduced.<br> — Improve accountability: As we report metrics for the execution of each module, each team can have their monitors and metrics based on the modules they own.<br> — Improve performance: Since each module is executed in parallel, the latency is now dictated by the slowest module in the screen, allowing us to introduce new modules inside the same screen without penalizing the overall performance.</li><li><strong>Modules</strong>: A Module is a plugin that can be injected into the template. Modules are independently developed features, which allowed us to scale up with the Videos, Social, and Picks features while keeping our system modular and maintainable. Each module handles its own:<br> — Backend logic and data retrieval: Each module includes specific logic for fetching relevant data and transforming it into a server-driven component.<br> — Monitoring and metrics: Individual modules log their metrics, which allows us to monitor and address module-specific issues without impacting the entire Store Wall.</li><li><strong>Data Providers</strong>: Once each module is executed in parallel, some may require the same data to calculate their business logic. As an efficiency measure, we introduced a proxy design pattern to data providers to ensure that each data request (e.g., store information, city metadata) is fetched only once per user request. For example, if three modules require store details, the data will be fetched once by a Proxy and shared across the modules, reducing redundant requests to backend services and improving response time/user experience.</li><li><strong>Server-Driven Components</strong>: The last piece of the architecture that enables high reusability is the Server-Driven UI layer that we injected on top of the plugin architecture. This layer acts as a support package used by Modules to render server-driven elements. This approach decouples backend logic from frontend dependencies, allowing us to reuse components across different modules/screens, which is essential for high-scale apps with numerous feature experiments and regional differences.</li></ul><p>Here, you can find an overview of the flow with all the components:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*PRN667v7WAI26ki0" /></figure><h3>Ensuring Stability and Scalability Under Tight Deadlines</h3><p>Even though our architecture allowed us to move forward fast and develop features independently, the critical nature of our 90-day deadline required meticulous dependency management and stability assurance, as each module had downstream dependencies where more traffic would be added. To understand that our features were production-ready and to ensure a stable release, here’s how we achieved simultaneous rollout of all three features:</p><ol><li><strong>Real-Time Monitoring</strong>: Each module collected metrics, enabling us to monitor and respond to issues in real-time. We set up dedicated alerts for each feature, ensuring a quick response to any emerging issues.</li><li><strong>Load Testing</strong>: We simulated traffic load across all features to understand system behavior under peak conditions, adjusting resource allocation to manage surges without compromising performance.</li><li><strong>Caching</strong>: Some modules share some of the data they need as input to process their logic. We could ensure minimum impact with caching strategies designed to manage parallel requests effectively.</li></ol><h3>Conclusion: Powering the Future of Delivery with a Modular Store Wall</h3><p>With several teams working on various features simultaneously, it was proven that its modular design not only enabled these parallel efforts but also minimized code conflicts and dependencies, contributing to a highly efficient and independent workflow. This decoupled approach ensured that new features could be added seamlessly without compromising app performance, allowing us to maintain a consistent user experience.</p><p>With features like Videos, Social, and Picks, we’re taking steps towards a more engaging, user-centric delivery app that enhances the user experience while preserving stability and scalability. This architecture will continue to support rapid feature introduction, evolving the Store Wall into a personalized, content-rich experience for all users.</p><p>Authors:</p><p><a href="https://medium.com/@VickyPerello">Victoria Perelló</a>, Software Engineer from Glovo</p><p><a href="https://medium.com/@hmalatini">Hernán Malatini,</a> Software Engineer from Glovo</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1e0b7f7dfdca" width="1" height="1" alt=""><hr><p><a href="https://medium.com/glovo-engineering/how-we-engineered-a-scalable-architecture-to-power-videos-social-and-picks-in-our-delivery-app-1e0b7f7dfdca">How We Engineered a Scalable Architecture to Power Videos, Social, and Picks in Our Delivery App</a> was originally published in <a href="https://medium.com/glovo-engineering">The Glovo Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building consistency at scale: Our journey with Compose Design System]]></title>
            <link>https://medium.com/glovo-engineering/building-consistency-at-scale-our-journey-with-compose-design-system-8a12b6d261be?source=rss----9bbfe5be0af5---4</link>
            <guid isPermaLink="false">https://medium.com/p/8a12b6d261be</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[api]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[compose]]></category>
            <dc:creator><![CDATA[Matias Isella]]></dc:creator>
            <pubDate>Tue, 12 Nov 2024 08:57:48 GMT</pubDate>
            <atom:updated>2024-11-12T13:26:32.025Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*SQbGuRaglIh45c-k7mZMdQ.png" /></figure><p>Modern applications must provide a seamless experience across all platforms. At Glovo, we have constantly faced this challenge and we’ve recognized the need for a unified Design System to ensure consistency across our growing products.</p><p>In this article, I will share our journey of creating a Compose Design System to support multiple applications at scale, focusing on API design. Similar to many other Design Systems, we identified several key characteristics which have influenced our decisions:</p><ol><li>Consistent: Ensures a similar user experience on both web and mobile.</li><li>Extensible: Allows for unique configurations and components while sharing a core experience.</li><li>Flexible: Enables teams to test new ideas without reinventing the wheel.</li></ol><h3>Google Material Design System — yes or no?</h3><p>If you’re an Android developer, chances are your first question is the same as ours. Should we use Material Design?</p><p>Material is one of the most used Design Systems in the world and ranks number one in most of the charts. When facing this question, I believe there are three main options, and lucky for us, they are <a href="https://developer.android.com/develop/ui/compose/designsystems/custom">documented</a> by Google.</p><h4><strong>Option 1. Extend Material</strong></h4><p>The first option is to use Material and extend it if needed. The main disadvantage of this approach is that the Design System API will contain the Material Design API. Therefore, the UX team needs to be onboard with the usage of the Material API (i.e. tokens and api definitions), as well with some inconsistencies that might occur between the designed Components and the Material Implementation.</p><h4><strong>Option 2. Replace Material</strong></h4><p>The second option is to replace Material. If your UX team requires a specific semantic token and you need to reuse Material components internally, replacing Material will give you the best of both worlds. Your Design System will expose only the tokens and components you define, and it will have access to Material components when needed. The main disadvantage of this approach is the maintenance overhead of managing a custom implementation while ensuring compatibility with Material updates.</p><h4><strong>Option 3. Not use Material</strong></h4><p>The last option is to not use Material. By taking this path, there is a significant disadvantage: you lose access to the constant development of Material. Hence, you cannot leverage Material for experimentation or to fill gaps in your Design System while working on the final Components. Unless you’re building an application from scratch, chances are you already have Material as a dependency. So we believe that you’re better off having access to the Material Component Catalog.</p><h4><strong>Decision</strong></h4><p>The second option was the one for us. Since our UX team required specific semantic tokens and we wanted to have access to the Material Components catalog internally, this approach best met our needs.</p><h3>Compose (+ View System?)</h3><p>Once we decided the fate of our Design System regarding Material, we faced our next question: to support or not support Android View System. Depending on your codebase, this question might not be relevant, but for us, it was critical. This determines the scope of the Design System as well as the availability and success of the project.</p><h4><strong>Option 1: Support View System</strong></h4><p>Supporting View System ensures compatibility with existing features and facilitates a smooth adoption of the Design System in older features. But, this approach may result in duplicated work for the Android Design System Team, since they would need to create two implementations for the same Component.</p><p>At the same time, there is a big risk of sinking time into working on Supporting View System Components, which might become outdated mid-term, with the added cost of delaying support for Jetpack Compose.</p><h4><strong>Option 2: Support only Compose</strong></h4><p>Alternatively, supporting only Jetpack Compose helps enforce the usage of Compose, teams can leverage this to push for adoption and foster consistency across the Project. But it’s worth mentioning that the adoption of the Design System will be limited by legacy features without Compose, since those will not not have access to the Design System.</p><p>The decision ultimately depends on the codebase. However, given the current state of Compose, there may be compelling reasons to prioritize support for Compose over the View System.</p><p>Taking the opportunity to highlight this great talk on Kotlinconf 2023. Related to Compose but focused on the learnings from adopting new technologies.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F6lBBpWX1x8Y%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D6lBBpWX1x8Y&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2F6lBBpWX1x8Y%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/e869b4edb7a0285a8eecd45e4d37e2ae/href">https://medium.com/media/e869b4edb7a0285a8eecd45e4d37e2ae/href</a></iframe><h3>Design System API</h3><p>As explained above, we will be focusing on the API, and based on our requirements and decisions at this point, we will be developing a Kotlin Compose API.</p><h4>Theme</h4><p>The Theme is the main entry point of the Design System and, at this point, it is expected in all Compose Design Systems to implement this pattern and expose a public Theme in their APIs. This pattern is built using three main components:</p><p>1. Theme Composable Function: This encapsulates all the theme properties, like colors and typography, and provides those to the corresponding composition locals in the composable tree.</p><pre>@Composable<br>public fun CoreTheme(<br>    colorScheme: CoreColorScheme = CoreTheme.colorScheme,<br>    typography: CoreTypography = CoreTheme.typography,<br>    content: @Composable () -&gt; Unit,<br>) {<br>    CompositionLocalProvider(<br>        LocalCoreColorScheme provides colorScheme,<br>        LocalCoreTypography provides typography,<br>        content = content<br>    )<br>}</pre><p>2. Theme Composition Locals: These allow for the static referencing of theme values in composable functions. Avoiding repeating or passing these as Composable functions parameters.</p><pre>internal val LocalCoreTypography = staticCompositionLocalOf { CoreTypography() }</pre><p>3. Theme Object: A singleton with the main purpose of increasing the discoverability of all the local compositions defined above.</p><pre>internal object CoreTheme {<br><br>    val colorScheme: CoreColorScheme<br>        @Composable<br>        @ReadOnlyComposable<br>        get() = LocalCoreColorScheme.current<br><br>    val typography: CoreTypography<br>        @Composable<br>        @ReadOnlyComposable<br>        get() = LocalCoreTypography.current<br>}</pre><p>For more details, find the full Theme Anatomy in the <a href="https://developer.android.com/develop/ui/compose/designsystems/anatomy">Google Docs</a>.</p><p>For us, this pattern provides the right level of scalability and flexibility. First, it gives the capability to wrap the main Theme to override the default values. Second, it allows the creation of new values based on the requirements of each Theme while sharing the core experience.</p><p>We highly recommend the <a href="https://developer.android.com/codelabs/jetpack-compose-theming-2">Compose Theming Codelab</a> to understand how theming works on Android. Although it uses Material 2 instead of Material 3, the basics of overriding a Theme, accessing the composition locals, and creating your own values are compatible.</p><p>Along with the Theme, the Design System must expose the components in its API. Ultimately, the goal of this API is to match the requirements from UX while minimizing the cognitive effort required by engineers to interpret the design.</p><h4>Content</h4><p>Composable APIs can follow a few different variants to deal with the Content of the Component.</p><p><strong>Option 1: Slot API</strong></p><p>From: <a href="https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md">Compose Component API Guidelines</a></p><p>This type of API is the most flexible, allowing the content to be any composable, and it is the recommended approach for all Composable APIs.</p><p>For us this type of API is used publicly for Layouts or Containers. And used internally for generalizing Components within the Design System to foster reusability. Exposing solely this type of API while increasing the flexibility of the Design System, could also increase the variants of each component causing a drop in Consistency.</p><pre>@Composable<br>private fun Avatar(<br>    // ...<br>    modifier: Modifier = Modifier,<br>    content: @Composable () -&gt; Unit,<br>)</pre><p><strong>Option 2: Restrictive API</strong></p><blockquote>Such API ensures that developers will be able to use the component only in the predefined way, leaving no space for possible mistakes and inconsistency.</blockquote><p>From: <a href="https://medium.com/bumble-tech/refining-compose-api-for-design-systems-d652e2c2eac3">Refining Compose API for design systems</a></p><p>This Compose API doesn’t have a Composable lambda in the method signature. The Component can only be used in a few ways restricted by the function parameters.</p><p>For us this type of API is good for removing ambiguity in the handover process of a new Design, since the content is highly opinionated and pre configured to look exactly like the UX Team defined, we only need to request the minimum variable portions of the Component. In our case, under the hood, these APIs always consume a Slot based one.</p><pre>@Composable<br>public fun Avatar(<br>    // ...<br>    modifier: Modifier = Modifier,<br>    painter: Painter,<br>)<br><br>@Composable<br>public fun Avatar(<br>    // ...<br>    modifier: Modifier = Modifier,<br>    text: String? = null,<br>)</pre><p><strong>Option 3: DSL based slots</strong></p><p>This pattern relies on the Composable lambda receiver Type to pass a Scope with Composable members.</p><blockquote>DSL for defining content of the component or its children should be perceived as an exception.</blockquote><p>From: <a href="https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md">Compose Component API Guidelines</a></p><p>For us this type of API is used when we need to tear down long APIs that have configuration for more than one subcomponent. As a rule of Thumb we always prefer to expose a Restrictive API with overloads than using a DSL Slot. The reason for this is that there is no limitation on invoking many members of the same custom Scope, potentially causing unexpected results.</p><pre>Composable<br>internal fun TextField(<br>    // ...<br>    start: @Composable (TextFieldContentDefaults.() -&gt; Unit)? = null,<br>    end: @Composable (TextFieldContentDefaults.() -&gt; Unit)? = null,<br>    // ...<br>)<br><br>@Stable<br>public object TextFieldContentDefaults {<br><br>    @Composable<br>    public fun Icon(<br>        // ...<br>    ) {<br>        // ...<br>    }<br><br>    @Composable<br>    public fun Text(<br>        // ...<br>    ) {<br>        // ...<br>    }<br>}</pre><p><strong>Option 4. Inverted Slot Api</strong></p><p>This type of API is reserved for Components that are expected to be Decorated. This method enforces a specific pattern where decorations are added before or after the inner Component.</p><p>The key difference between using a “Inverted” Slot Api and a Slot Api, is that it is expected for the Decoration to share the same behavior as the inner Component.</p><pre>@Composable<br>fun BasicTextField(<br>    // ...<br>    decorator: TextFieldDecorator? = null,<br>    // ...<br>)<br><br>fun interface TextFieldDecorator {<br><br>    @Composable<br>    fun Decoration(innerTextField: @Composable () -&gt; Unit)<br>}</pre><h4><strong>Text</strong></h4><p>The text content is one of the most used in the API definition, as many components in a large design system will receive string content.</p><p>From an API perspective, the approach depends on your use case, and ultimately, there are two options: either expose an AnnotatedString overload or not.</p><pre>@Composable<br>public fun Banner(<br>    body: String,<br>)<br><br>@Composable<br>public fun Banner(<br>    body: AnnotatedString,<br>)</pre><p>The main difference between the two signatures is that by using AnnotatedString, your API opens the capability for overriding TextStyle attributes, potentially causing unexpected TextStyles that are not defined in the Design System Typography.</p><p>Additionally, note that without getting into the implementation details, regardless of your API, all text will fall under one of these Compose modifiers: TextStringSimpleElement or TextAnnotatedStringElement. The second one is slower than the first one. When possible, prefer to use different BasicText components, one for the AnnotatedString and one for the String.</p><h4>Style</h4><p>We refer to Style as those parameters or functions used in our Design System API to define the look and feel of a component. Usually, Style parameters or functions are key elements in the handover process from UX to Engineering. These should be consistent across platforms and UX tooling to produce the same output.</p><p>Regardless of the process for building styles (e.g., manual or code generation), we have identified at least three main options in the API with styleable Components.</p><p><strong>Option 1: Closed Styles</strong></p><p>This is the simplest approach. The main advantage is that your Style constructor API visibility ensures no new styles will be created, making it closed for extension by design. The main disadvantage is that ad hoc styles for experimentation are not possible.</p><pre>@Composable<br>public fun Avatar(<br>    style: AvatarStyle,<br>)<br><br>public enum class AvatarStyle(internal val shape: CornerBasedShape) {<br>    SQUARE(RoundedCornerShape(64.dp)),<br>    CIRCLE(CircleShape)<br>}</pre><p>or</p><pre>@Composable<br>public fun Avatar(<br>    style: AvatarStyle,<br>)<br><br>public enum class AvatarStyle {<br>    Square,<br>    Circle,<br>}</pre><p><strong>Option 2: Open Styles</strong></p><p>The main disadvantage of this option is the lack of exhaustiveness in the evaluation of styles, which can be useful for some presentation use cases. The main advantage is that it facilitates easier collaboration for consumers of the Design System and allows for simpler experimentation.</p><pre>@Composable<br>public fun Avatar(<br>    style: AvatarStyle,<br>)<br><br>public data class AvatarStyle(internal val shape: CornerBasedShape)<br><br>public data object CoreAvatarStyle {<br>    public val Square: AvatarStyle = AvatarStyle(RoundedCornerShape(64.dp))<br>    public val Circle: AvatarStyle = AvatarStyle(CircleShape)<br>}</pre><p>There is no compose stability difference between Open and Closed Styles as long as they encapsulate stable parameters.</p><p><strong>Option 3: Multiple components</strong></p><p>The previous two approaches use parameters for styling the component. Although this is easier from a handover perspective and for experimentation, the recommended convention is to specify separate @Composable functions with different names.</p><blockquote>Express dependencies in a granular, semantically meaningful way. Avoid grab-bag style parameters and classes, akin to ComponentStyle or ComponentConfiguration.</blockquote><p>From: <a href="https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md#prefer-multiple-components-over-style-classes">Compose Component API Guidelines</a></p><p>The main advantage of this approach is that the semantic meaning is clear and doesn’t need to be unwrapped. The main disadvantage is that it is harder to discover these separate functions compared to using a style parameter.</p><pre>@Composable<br>public fun PrimaryAvatar() {<br><br>}<br><br>@Composable<br>public fun SecondaryAvatar() {<br><br>}</pre><p>In the example above, specifying an avatar as either square or circle does not carry inherent semantic meaning in the function. On the contrary, designating an avatar as primary or secondary conveys semantic meaning, indicating their importance and intended usage in the UI.</p><h4><strong>Modifiers</strong></h4><blockquote>Every component that emits UI should have a modifier parameter.</blockquote><p>Based on the <a href="https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md#modifier-parameter">Compose component API guidelines</a>, every public Composable API should expose a Modifier.</p><p>Exposing a Modifier in the API ensures flexibility and consistency due being able to allow adding functionality without actually changing the Component.</p><p>Modifiers in APIs are expected to be at a certain position in the parameters: right after the required parameters and before the first optional parameter.</p><blockquote>Why? Required parameters indicate the contract of the component, since they have to be passed and are necessary for the component to work properly. By placing required parameters first, API clearly indicates the requirements and contract of the said component. Optional parameters represent some customisation and additional capabilities of the component, and don’t require immediate attention of the user.</blockquote><p>Note that missing a Modifier in the API imposes several restrictions regardless of the content, such as testability or accessibility, which rely on the <a href="https://developer.android.com/develop/ui/compose/accessibility/semantics">semantic tree</a> to be achievable. And this semantic tree is built using the <a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/semantics/package-summary#(androidx.compose.ui.Modifier).semantics(kotlin.Boolean,kotlin.Function1)">semantics</a> Modifier.</p><h4><strong>Stability</strong></h4><p>When building a Composable library at scale, it is desirable that most components are skippable due to being small units within the UI. A Composable Design System shouldn’t impact recomposition.</p><p>From an API perspective, using <a href="https://developer.android.com/jetpack/androidx/releases/compose-foundation">Compose foundations</a> ensures that the classes used as parameters are stable, making the components skippable. For us, most of the instability usually comes from using unstable parameters such as List or Painter. Overall, running regular Compose compiler reports to catch any regressions has been effective.</p><pre>composeCompiler {<br>   enableStrongSkippingMode = true<br>   reportsDestination = file(&quot;build/reports/compose&quot;)<br>}</pre><p>Note that a strong skipping mode would solve many of these caveats.</p><h4>Documentation</h4><p>As any API, it is expected to have good code documentation providing relevant content to Developers using the Design System. And <a href="https://kotlinlang.org/docs/kotlin-doc.html">KDoc</a> offers several attributes that help developers understand and navigate your API more effectively.</p><p>In addition to the common @parameter and @see tags, which describes how inputs affect the component’s behavior and provide links to relevant classes or methods, the @sample tag is particularly important in a Design System API.</p><p>The <a href="https://kotlinlang.org/docs/kotlin-doc.html#sample-identifier">@sample</a> tag has many relevant features for us:</p><ol><li>It gives the developer an immediate example of how to use the Component.</li><li>The sample is a Composable @Preview, providing the developer with an immediate preview of the Component.</li><li>Samples are also compiled, offering an integration test out-of-the-box for free.</li><li>Samples are included as Code Blocks when using a documentation engine.</li></ol><p>Finally, a clear benefit of using KDocs is the ability to export this documentation for public availability by generating it in HTML and Markdown using <a href="https://kotlinlang.org/docs/dokka-introduction.html">Dokka</a>.</p><h4>Bonus: Explicit API</h4><p>By using strict explicit API, we ensure consistency in the codebase since developers must follow the same conventions. Developers are forced to think about each API’s visibility. This leads to better designed APIs, build time check, and self documented code through the use of explicit visibility modifiers.</p><pre>kotlin {<br>   explicitApi()<br>}</pre><h3>Summary</h3><p>In summary, these are the key points we would like to highlight:</p><ul><li>Material Design: We don’t use it directly. Instead, we reuse Material components when needed, but these are internal to the Design System.</li><li>Legacy View System: Not supported. We focus only on Jetpack Compose and use the Design System to leverage increased adoption of Compose.</li><li>API Flexibility: We use different approaches depending on the required flexibility. However, we prefer to be opinionated as much as possible and expose the minimum number of parameters to prevent unexpected variations of components. In line with this, we use Closed Styles to ensure exhaustive evaluations and to make sure no use case falls through the cracks.</li><li>API Documentation: We heavily rely on KDoc to explain the components to Design System users, providing good code examples.</li><li>API Visibility: Using explicit API has been key to maintaining consistency.</li></ul><p>This is just the first step of our journey. We are leaving the full implementation of all these APIs outside of this initial article. Diving deeper into this topic will require a few more articles, so for now, we will leave it here. Thank you for reading, and stay tuned for future updates where we’ll explore these concepts in more detail.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8a12b6d261be" width="1" height="1" alt=""><hr><p><a href="https://medium.com/glovo-engineering/building-consistency-at-scale-our-journey-with-compose-design-system-8a12b6d261be">Building consistency at scale: Our journey with Compose Design System</a> was originally published in <a href="https://medium.com/glovo-engineering">The Glovo Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Slashing Lead Times by 30%: The Impact of Using Explicit Types in JVM]]></title>
            <link>https://medium.com/glovo-engineering/slashing-lead-times-by-30-the-impact-of-using-explicit-types-in-jvm-932b46a0f07d?source=rss----9bbfe5be0af5---4</link>
            <guid isPermaLink="false">https://medium.com/p/932b46a0f07d</guid>
            <category><![CDATA[explicit-types]]></category>
            <category><![CDATA[gradle]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[compile]]></category>
            <category><![CDATA[jvm]]></category>
            <dc:creator><![CDATA[alexxozo]]></dc:creator>
            <pubDate>Mon, 18 Dec 2023 10:31:49 GMT</pubDate>
            <atom:updated>2023-12-18T11:35:57.295Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>Quick summary:</strong> As a developer, there are few things more exasperating than waiting for your code to compile. Today we’ll delve into the steps we took to address a significant increase in build time for one of our microservices. By just <strong>adding explicit types to java.Map definitions</strong>, we managed to<strong> cut down our build time by 30%</strong>!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*trAnVsrLSxlgaiuN" /></figure><h3>👀 Context</h3><p>In Glovo, the home and store feed screens act as the primary entry points for our ordering process. It’s crucial for us to<strong> prioritise rapid deployment </strong>in the service delivering these contents, it helps us <strong>experiment fast and ensure swift responses in case of incidents</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*x4NLQbJDbZq0kZaVnTSMNA.png" /></figure><p>One of our technical objectives was to improve the <strong>lead time of the development process</strong>.</p><p><strong><em>Lead time refers to the duration from when a code change (commit) is initially made to the point where it’s fully implemented in the production environment</em></strong><em>. </em><strong><em>This process involves several stages, such as coding, testing, code reviews and deployment.</em></strong></p><p>By <strong>reducing it</strong>, we can increase efficiency, allowing for updates to reach the end users faster. This will translate to<strong> faster product experimentation</strong> which in turn makes us better at <strong>delivering value to our customers</strong>. Our 35 minute build process, followed by a 7-minute deployment phase, was more of a roadblock than a speedway! ❌</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*p3QSG3BkcmVM9V76" /></figure><h3>⭐ The goal: Make it fast!</h3><p>One of our Glovo values is GAS — <em>“We work hard and execute fast. We always ask ourselves ‘How can this be done faster?’”</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/348/1*Ymd_p2SAEsA27RzHoyKoQw.png" /></figure><p>We needed to <strong>decrease that 36 mins CI pipeline</strong> as much as possible!</p><p>But first, a little backstory: previously we have been switching from Jenkins to GitHub Actions (GHA) pipelines, the main reason being <strong>resource/cost optimisation</strong> and ease of use. The catch for us was that in Jenkins we had gigantic instances running the build, however in Github actions, even the largest runners were optimized for resource usage and had set memory limits!</p><p>Since we had a lot of integration tests covering most critical flows, <strong>this exposed scalability issues in the integration tests</strong>, they were simply consuming <strong>too much memory</strong>. Even before moving to GHA, our optimization journey had begun.</p><h3>📚 Memory consumption</h3><p>The <strong>builds in GHA were failing due to out-of-memory errors </strong>(ie. some tests were taking too much memory). We’ve temporarily stopped the migration process from Jenkins to GHA and started thoroughly investigating the <strong>performance of our integration tests</strong>.</p><p>While this topic is beyond the scope of this post, I’d like to mention some of the steps we took for optimizing tests:</p><ul><li>Identifying <strong>tests lacking <em>org.springframework.context.annotation.profile</em> and reusing context</strong>, ensuring they now use the common TEST profile</li><li><strong>Isolate problematic tests</strong> that were causing the memory issues. Eg. some were using <strong>mockbeans for mocking an external client</strong>. This impacted the reusability of contexts, resulting in recreating them for each test, increasing the memory footprint</li><li>Some of the <strong>unit tests were also extending the integration bases class</strong> <strong>(a lot of dependencies)</strong> and this was a waste of resources and execution time</li><li><strong>Cleanup</strong> of unused integration tests</li></ul><p>Finally we have also tweaked our gradle build parameters to obtain better performance overall (<strong>memory and time reduction</strong>)</p><pre>tasks.withType(JavaCompile) {<br>  options.compilerArgs &lt;&lt; &quot;-Xlint:-options&quot;<br>  options.encoding = &#39;UTF-8&#39;<br>  options.fork = true<br>  options.forkOptions.setMemoryMaximumSize(&quot;8g&quot;)<br>  options.incremental = true<br>}<br>org.gradle.parallel=truegr<br>org.gradle.caching=true</pre><p>Most notably were:</p><p><strong>options.fork = true</strong></p><p><em>When fork is set to true, the Java compiler runs in a separate process. This can be useful for several reasons, such as avoiding memory issues in the Gradle daemon process, or isolating the compile process from Gradle’s own classpath.</em></p><p><strong>options.incremental = true</strong></p><p><em>Incremental compilation allows Gradle to recompile only the parts of the code that have changed since the last build, which can significantly speed up the build process.</em></p><p><strong>org.gradle.parallel=true</strong></p><p><em>Allows Gradle to execute multiple tasks in parallel. This can greatly improve the build speed, especially on multi-core machines, as it makes better use of available CPU resources.</em></p><p><strong>org.gradle.caching=true</strong></p><p><em>The build cache can significantly reduce build times by reusing outputs from previous executions of tasks. For instance, if a task has already been executed with the same inputs (source files, configuration, etc.), Gradle can skip its execution and use the cached result instead.</em></p><p>By combining all these methods we’ve managed to migrate Jenkins to GHA successfully! 🚀</p><h3>⏱️ Compilation time</h3><p>Even though tests were optimized, after a while we’ve come to realize that <strong>build time is still painfully large</strong> so we turned our heads towards the compilation time. And like this, we’ve embarked on another journey to discover <strong>how to make our code compile FAST</strong>!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*QfGRP02WJQozhkIA" /></figure><p>To get a better grasp on the issue we decided to utilize Gradle’s debugging options to dive into the underlying reasons for the prolonged build times.</p><ul><li><strong>Divide and Conquer — </strong>The first step was to isolate the slow performing action in the build process. Initially there was a single Github Action for build and test. We’ve split it into separate execution steps for build, unit tests and integration tests. This gave us a great insight, the <strong>main driver for compilation time seemed to be the BUILD step 🚀</strong></li><li><strong>Searching for a Problematic Commit</strong> — Next, we’ve <strong>inspected the commits made in the past weeks</strong> to check if there is anything that could lead to more compilation time. Unfortunately we could not find anything on this route…</li><li><strong>Trial and Error with Gradle commands</strong> — We tried using some of the common gradle build commands to check if they help, unfortunately nothing really helped…some of them (might be useful for your scenario):</li></ul><p><strong>gradle build — scan</strong></p><p><em>Gradle collects data about the build, such as how long tasks took to execute, what tasks were executed, and information about the environment (like Gradle version, Java version, operating system, etc.).</em></p><p>We ran this command locally and even though the analysis was exhaustive and it highlighted compile java took a lot of time, it could not narrow down to the level of classes.</p><p><strong>gradle build — stacktrace</strong></p><p><em>A stack trace is a report of the active stack frames at a certain point in time during the execution of a program. It’s particularly useful for debugging because it shows the call sequence that led to the error or exception.</em></p><p>Since this just provides a summary of tasks and might be useful when there are errors, which wasn’t the case, this was again not helpful.</p><p><strong>gradle build — info &amp; gradle build — debug</strong></p><p><em>Tells Gradle to provide more detailed output than usual.</em></p><p>This once again gave a detailed description of the build, tasks executed, dependency resolution etc, but once again was not enough to narrow down the root cause.</p><ul><li><strong>Trial and Error with JavaCompile Options</strong> — The <strong>WINNING</strong> <strong>command</strong> was <strong>options.verbose </strong>🚀! Because it gave us the needed visibility to know why compilation takes so much time. This is the final configuration we’ve used:</li></ul><pre>tasks.withType(JavaCompile) {<br>  options.compilerArgs &lt;&lt; &quot;-Xlint:-options&quot;<br>  options.encoding = &#39;UTF-8&#39;<br>  options.fork = true<br>  options.forkOptions.setMemoryMaximumSize(&quot;8g&quot;)<br>  options.incremental = true<br>  options.debug = true<br>  options.verbose=true<br>}</pre><p>This option provided detailed information about the compilation step, and we noticed that for a particular class, the compilation step took more than <strong>5 minutes</strong>! <strong>The begin and end logs of that class showed this duration (see image below).</strong> We executed this a few times to confirm this was not random.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*sClf6RJJaFZzDXSl" /></figure><p>The class was quite simple, it was just defining some java.Map objects that hold static data for some of our experiments. The class looked something like this:</p><pre>public static final Map&lt;String, String&gt; ABTestData = Map.ofEntries(<br>     Map.entry(&quot;A&quot;, &quot;B&quot;),<br>     // 500+ entries<br>);</pre><p>Our breakthrough came from <strong>a thread from</strong> <strong>2019 in the OpenJDK mailing list</strong> (see <a href="https://mail.openjdk.org/pipermail/compiler-dev/2021-July/017609.html">OpenJDK Mailing List</a>). Extract from the message:</p><pre>In javac we are doing a lot of heroics to try and keep the space of <br>inference variable as small as possible, by aggressively de-duping <br>inference variables where possible. This strategy works well in cases like:<br><br>a(b(c(d(...)))))<br><br>But in cases like the ones you report (or those in the JBS issues <br>above), which have a shape like:<br><br>a(b(), c(), d(), e() ... )<br><br>We do not yet perform any de-duping, so the inference engine has to run <br>with a very big set of (possibly very similarly looking) inference <br>variables. Performing incorporation will end up setting similarly <br>looking bounds on the inference variables of the outer a() call, which <br>all have to be validated, and so on and so forth.</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*_eaPFGUCKReBss_7" /></figure><p><strong>This was our AHAAAAA moment</strong>! 🚀</p><p>When you do not explicitly specify the type parameters (like Map.&lt;String, String&gt;ofEntries), the compiler has to infer them. This is straightforward for a few entries but can become increasingly complex and resource-intensive as the number of entries grows (our situation).</p><pre>public static final Map&lt;String, String&gt; MAP = Map.&lt;String, String&gt;ofEntries(<br>     Map.entry(&quot;A&quot;, &quot;B&quot;),<br>     // More entries<br>);</pre><p>By explicitly specifying the type parameters (e.g. using Map.&lt;String, String&gt;ofEntries), we relieved the compiler of the need to infer the types for each entry, significantly speeding up the compilation process as the compiler no longer needs to perform inference for each entry. <strong>🚀🚀</strong></p><h3>💡 The Result: Reduced Compilation Time by 30%</h3><p>This modest code adjustment led to a <strong>substantial 30% decrease in our build time</strong>! The impact was immediate! In our fast-paced environment <strong>every minute counts</strong>, and now, suddenly, we have<strong> cut down our waiting time by almost a third.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/947/0*z8kRSiViIaqE_hDQ" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/872/0*F_JeXgnRoqWQc-PI" /></figure><h3>🚀 Conclusion</h3><p>In this article we’ve learned about Gradle, GHA, Jenkins, JavaCompile options and the inner workings of JVM for the purpose of optimizing the build times of our microservice. I hope you’ll find some valuable information here that will help you do the same!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=932b46a0f07d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/glovo-engineering/slashing-lead-times-by-30-the-impact-of-using-explicit-types-in-jvm-932b46a0f07d">Slashing Lead Times by 30%: The Impact of Using Explicit Types in JVM</a> was originally published in <a href="https://medium.com/glovo-engineering">The Glovo Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Taste the World: How Our New Machine Translation Feature Transforms Your Ordering Experience]]></title>
            <link>https://medium.com/glovo-engineering/taste-the-world-how-our-new-machine-translation-feature-transforms-your-ordering-experience-5d73f1efb2b3?source=rss----9bbfe5be0af5---4</link>
            <guid isPermaLink="false">https://medium.com/p/5d73f1efb2b3</guid>
            <category><![CDATA[system-architecture]]></category>
            <category><![CDATA[innovation]]></category>
            <category><![CDATA[machine-translation]]></category>
            <category><![CDATA[translation]]></category>
            <category><![CDATA[order-food-online]]></category>
            <dc:creator><![CDATA[Ahmad Hamouda]]></dc:creator>
            <pubDate>Wed, 13 Dec 2023 09:18:09 GMT</pubDate>
            <atom:updated>2023-12-13T09:47:22.353Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/720/0*tcaiZWN2tJatLVuw" /></figure><h3>Authors:</h3><p>Ahmad Hamouda, Software Engineer IV at Glovo<br>Stefania Russo, Head of UX Content at Glovo</p><h3>Introduction</h3><p>Glovo’s mission is to “give everyone easy access to anything in their city”. Being live in 24 markets, we aim to provide a seamless experience without language barriers for all our customers.</p><p>While the interface of our apps and platforms are already localized in the languages of every country we operate in, there was still one more language barrier to overcome: our restaurant menus and store product lists, which are monolingual, were not translated into the user preferred language.</p><p>Imagine: you’re a native English speaker who lives in Barcelona and doesn’t speak Spanish. Your phone is in English and so is the Glovo app.</p><p>You are hungry on a Friday night and while browsing Glovo find all restaurant menus are either in Spanish or Catalan. There is no simple way in the app to translate the menu and having to copy/paste into Google Translate is frustrating. You’ll likely end up ordering something that you’re not sure of or close the app and go pick up the food yourself.</p><p>By giving our users the possibility to see a translated version of our menus in the language of their mobile devices, we can increase our reach and penetration in every market as well as improve the overall user experience.</p><p>In this article, we will give an overview of the solution we built, a deep dive into the localization challenges, and how we are measuring success.</p><h3>Customer pain point</h3><p>Glovo operates in countries with a high percentage of immigrants, so a monolingual catalog automatically prevents a significant number of users from placing an order due to language inaccessibility.</p><p>The lack of a translation solution for our restaurant menus and store product lists was already a known major pain point in many of Glovo’s countries.</p><p>On top of this, whenever a user had their phone in a language that was different from the country’s main language, they were exposed to an unexpected multilingual experience they did not choose.</p><h3>Users prefer content in their language</h3><p>In 2020, the content and language services firm CSA Research published <a href="https://csa-research.com/Featured-Content/For-Global-Enterprises/Global-Growth/CRWB-Series/CRWB-B2C">”Can‘t Read, Won‘t Buy”</a> summarizing people‘s attitudes towards using products in their language versus other languages. The results were eye-opening:</p><ul><li>65% prefer content in their language, even if it‘s poor quality</li><li>67% tolerate mixed languages on a website</li><li>73% prefer products with information in their own language</li><li>66% use online machine translation</li><li>40% will not buy from websites in other languages.</li></ul><h3>Our Product catalog challenges</h3><h3>Catalog ownership</h3><p>Menu and store catalogs are owned by the partners and even though clear global guidelines are provided, partners choose the language and format of their catalogs.</p><h3>Language mix and language detection</h3><p>While most countries have catalogs in the same local language, some partners in multilingual countries choose to duplicate their menus on Glovo to offer a multilingual experience to their customers (ie. in Georgia many restaurants have their menus in both Georgian and Russian).</p><p>This quick solution on the partners’ side, to compensate for the lack of a translation feature for menus, becomes a challenge when trying to establish a scalable translation feature for menus. Most menu items consist of one or two words and language detection in such small units of text is a big technical challenge.</p><h3><strong>Catalog dimensions and updates</strong></h3><p>Our restaurant and store catalog contains millions of products and they undergo frequent updates.</p><p>Menus and product catalogs are structured into names (ie. Pizza Margherita) and descriptions (ie. ingredients like tomato, mozzarella, or basil). Partners can also organize content in sections, collections, and super-collections (ie. Top sales, Combos, Starters, Salads, Classic pizzas, etc.).</p><p>Catalogs are updated frequently depending on the type of business: seasonal menus, special offers, changes in product offerings, etc.</p><p>These updates occur across all markets, for all partners, every single day. Millions of menu and catalog entries are updated daily.</p><h3>The Solution</h3><p>It was clear that what we needed was a real-time Machine Translation (MT) solution, integrated into our systems. We aimed to translate our menus and catalogs into the user’s device language via an API; without requiring any human intervention.</p><p>As the result of an effective and rewarding cross-team collaboration between <strong>Localization</strong>, <strong>Engineering</strong>, and <strong>Product</strong> we closed the gap between the localized interface and the product catalog language.</p><h3>Getting Started</h3><p>We collected all the necessary requirements to select a third-party Machine Translation provider that could fit our needs.</p><p>Main considerations:</p><ul><li>Machine Learning Model customization: while the Machine Translation had to happen real-time, we needed a system to customize the machine learning process based on Glovo-specific content requirements and existing content</li><li>Machine translation coverage for not-so-common language combinations like Armenian and Georgian into English and Russian</li><li>Adaptive technology that could quickly learn from user feedback</li><li>Low latency and high availability: maintaining low latency for personalized customer experience and stringent SLAs to ensure service reliability to avoid customer experience degradation</li><li>Quality monitoring</li><li>Scalability</li><li>Cost-effective solution</li><li>Robust data processing capabilities.</li></ul><h3>Our Machine Translation Partner</h3><p><a href="https://www.modernmt.com/"><strong>ModernMT</strong></a> is the provider who won the selection process, as it met our requirements in terms of tech solution, quality customization needs, and cost effectiveness.</p><p>ModernMT is an <strong>adaptive neural machine translation system</strong> and one of the top-rated in the market, developed by <a href="https://translated.com/welcome">Translated</a>.</p><p>ModernMT was recently recognized as a leader in the<a href="https://translated.com/machine-translation-leader-IDC"> IDC MarketSpace</a> for machine translation software, ahead of the likes of Google, Amazon, and Microsoft. Currently, ModernMT supports 200 languages, reaching over 6.5 billion native speakers worldwide.</p><h3>The MVP</h3><p>Enabling machine translation of the product catalog touches many phases of the customer journey from the moment a customer starts their search for a product until they pay for their order at checkout.</p><p>We adopted a lean approach to get started, so we prioritized enabling the machine translation feature on the<strong> store screen [Figure 1] </strong>to make sure our customers could get the most out of it.</p><p>We adopted the same lean approach when deciding where to first roll out the feature. For the aforementioned reasons, we began with two countries most in need for a machine translation solution: <strong>Georgia </strong>and<strong> Armenia</strong>.</p><p>After the first roll-out and applying a number of learnings from the initial trial, the feature was scaled to the store pages/screens for the remaining countries.</p><p>We determined the language combinations for enabling the machine translation feature based on the most used device languages in each country.</p><h3>Overview of the feature</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/375/0*Shm8Fi6RsMkmyfna" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/377/0*7ZSozh9SlSDukZeA" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/375/0*NfkM-Ih_q1RwQe-6" /></figure><p>Figure 1</p><h3>Localization deep-dive: the Machine Translation Customization effort</h3><p>Customizing a machine translation engine means providing it with relevant material so it learns from it and becomes better over time.</p><p>In our case, it involved injecting samples of correct product translations and language glossaries into the engine so that it learns from them and applies the learnings across the whole system.</p><p>This practice is extremely important when the content to machine-translate consists of short units of text, such as menus and product lists, because the machine doesn’t have a lot of contextual text to support its choices. Long sentences or paragraphs provide the machine with better context and therefore imply less training and a faster learning process.</p><p>The customization work has been divided into the following phases:</p><h4>Phase 1: Machine Translation Engine Pre-training</h4><p>MT engines are fed with the following datasets for each language combination:</p><ul><li>Existing translation memory databases</li><li>Exports of top-selling products for each language and market</li><li>Do-Not-Translate glossaries (a list of terms which we never want to translate)</li></ul><h4>Phase 2: Sample Human Reviews</h4><p>ModernMT learns dynamically and continuously. So, beyond the pre-training step above, we extracted samples of our product catalogs for each market, processed them through the engine, and had human translators perform a linguistic review.</p><p>This step allowed us to directly feed corrections and feedback directly into the ModernMT engine.</p><h4>Phase 3: Machine Translation Glossaries</h4><p>A glossary, in the context of Machine Translation, is a tool that facilitates the consistent translation of customer-specific terminologies, giving advanced control over the terms used.</p><p>ModernMT has an MT Glossary feature integrated into their API. This allows us to create Glovo-specific terminology lists per country that can help us boost the quality and nuances of the machine translation engine output.</p><p>Our MT glossaries include:</p><ul><li>Universal food terminology (ingredients, dishes, generic products, etc.)</li><li>Local ingredients</li><li>Local dishes</li><li>Big Food chains</li><li>Q-commerce products</li></ul><p>The MT glossaries are a live asset, which will be updated regularly to make sure we keep up with the product catalog updates.</p><h4>Phase 4: Continuous Feedback Loop</h4><p>Being able to quickly implement any feedback is crucial for us and for our end users.</p><p>For this reason, we implemented an in-app feedback solution thanks to which Glovo employees using our beta app are able to easily report any wrong machine translation by clicking on a button.</p><p>The clicking of the button sends synchronized API requests along with necessary metadata to the TranslationOS platform, which triggers a human review by a professional translator.</p><p>Once the review is completed we receive the corrected text back via API, which is then integrated internally into our ModernMT model.</p><p>This feedback loop combined with the adaptive MT model aims at a continuous improvement of our solution.</p><h3>The Engineering deep-dive: building a dedicated microservice</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/785/0*WIJsNCIV6MBqFF9a" /></figure><p>At first glance, connecting third-party services directly with ours may appear straightforward. However, our stringent requirements related to security, data control, quality assurance, and cost reduction make this integration more complex than it seems.</p><p>Our primary goal is to maintain exceptionally low storage latency while minimizing it as much as possible, all while retaining control over our Service Level Agreements (SLAs).</p><p>To navigate these challenges, we’ve implemented an asynchronous approach to manage client requests.</p><p>When a client queries us, we promptly provide the data if it’s available. If not, we notify the client of data unavailability while presenting the original text.</p><p>Simultaneously, we initiate an asynchronous request to ModernMT for the translation, storing it in our database. As a result, subsequent requests for the same word and language combination are instantly served from our storage. Although this method incurs a failure for the initial translation request, it significantly reduces costs by approximately 95%.</p><p>This cost reduction is attributed to two key factors:</p><ol><li>We translate only a minimal part of our entire catalog (items visible to users).</li><li>We reuse the same translation without incurring additional translation costs for every subsequent request.</li></ol><p>We use a scoring system to decide when to display translated content. If 85% of our items are translated, we show the translations. This helps us ensure that our pages stay relevant, and we can adjust the threshold based on our evolving business needs.</p><p>To make things easier for our users, we’ve introduced an auto-translation feature. When a user’s device language matches one of the supported languages configured for their current country (ie. your device language is English and you’re in Spain where English is a supported target language), the translation happens automatically without any input from the user.</p><p>We’ve designed this to accommodate different language preferences, providing an inclusive experience for all users.</p><h3>Tech Mid-Level Solution: Making Informed Technology Choices</h3><p>As we planned our service, we started by figuring out how much storage we needed, predicting the traffic mainly from the store screen, and setting our SLA goals.</p><p>In this process, we looked at three options, each with its own pros and cons.</p><h4>Database:</h4><p>Redis:</p><p>Redis emerged as an initial contender due to its cost-effectiveness compared to DynamoDB and its superior speed when compared to a standalone MySql setup. However, its challenge lies in data persistence. While there are more advanced options available, such as Elasticache with persistence, they come with increased costs.</p><p>DynamoDB:</p><p>Although DynamoDB offered speed, ensuring stable access patterns and understanding Read Capacity Units (RCU) and Write Capacity Units (WCU) were critical requirements.</p><p>SQL:</p><p>We considered SQL solutions, which seemed cost-effective, but using them might require adding Redis for extra features. After careful thought, we decided to start the service with Redis. This lets us gather data on reads and writes, validate new features, and plan for the future based on data. Our iterative approach allows us to continuously improve the project.</p><p>Even though we designed models for DynamoDB and SQL, we structured our data model so that switching from Redis to either DynamoDB or SQL in the future is still possible.</p><p>This decision gives us the flexibility to adapt based on metrics and user feedback, ensuring the service remains reliable and efficient.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/847/0*aW-80cLEaMogFgBD" /></figure><h4>Communications: Streamlined Machine Translation Services</h4><p>To improve our machine translation services, we divided our system into two separate modules. Each module focuses on key aspects, including deployment, source code, and infrastructure:</p><p>Event-Based Module</p><p>This module functions on an event-driven architecture, allowing asynchronous communication and enabling the retrieval of translations from our service providers. This setup ensures effective handling of various translation requests by separating functionalities, promoting scalable, and independently configurable operations.</p><p>API Module</p><p>On the flip side, our API module handles user traffic and oversees fallback from TranslationOS, guaranteeing a responsive interface for users. This split allows for customized scaling and the use of distinct configuration libraries. This modular approach results in cleaner code, a secure platform, and improved organization. It also lays a solid foundation for adapting to future changes and scaling requirements.</p><p>Our reliance on Kafka, an asynchronous technology within Glovo, ensures the smooth operation of our event-based module. Additionally, we’ve incorporated robust features like retries, limiters, and circuit breakers in our communication protocols with ModernMT and TranslationOS. These measures are crucial for adhering to their limitations, respecting service capacities, and managing fallback scenarios effectively.</p><p>This detailed approach not only ensures optimized communication channels but also strengthens our system’s resilience, guarding against potential service disruptions and enhancing overall reliability.</p><h3>Clean Up and Refresh Data</h3><p>We manage our data in Redis with specific Time-to-Live (TTL) settings, which we refresh based on usage patterns. This means that frequently accessed items have their TTLs extended. However, if we update it with every access, we will have too many unneeded updates on our storage. For this reason, we decided to use a statistical model to extend the TTL only [ex: randomly once every 10 accesses]. This helps reduce write operations and overall costs by keeping them available for a longer time.</p><p>Moreover, our feedback loop lets us update and enhance specific items with poor translation quality as explained earlier.</p><p>As our machine learning model progresses, there comes a time when data cleanup becomes necessary. To address this, we’ve created an internal API for data cleansing based on language pairs or countries. This API becomes active when we reach a specific threshold or introduce a significant amount of new data to our glossary, ensuring efficient maintenance management.</p><h3>Security</h3><p>Ensuring security is a top priority for us, especially when dealing with third parties and managing incoming data that directly affects our company’s reputation and cost center. To minimize risks related to these interactions, we perform thorough risk analyses and make informed choices regarding password rotation, sharing practices, and identity validation.</p><p>Through close collaboration with ModernMT, we’re implementing crucial changes related to passwords and tokens. Their support and flexible approach have proven invaluable in strengthening our security measures to protect our systems and data.</p><h3>Measuring Success: Impact in Numbers</h3><p>Keeping track of the metrics below is pivotal in measuring the impact of our machine translation solution. This will help us refine strategies, confirm impact, and continuously improve the service based on user behavior and feedback.</p><h3>Conversion Rate (CVR)</h3><p>CVR is a key measure to evaluate how well our translation services work. By comparing the conversion rate before and after implementing translations, we can see how it affects user engagement and actions like purchases or interactions on the platform.</p><h3>New Customer Acquisition</h3><p>Measuring new customer acquisition serves as a lever in attracting foreign customers and expats to our platform. Tracking the influx of new users post-translation implementation helps quantify the service’s effectiveness in broadening our user base. It provides concrete data on how well our translation solutions resonate with a diverse audience, reflecting our ability to attract and retain foreign users, expatriates, and newcomers to the platform.</p><h3>Served Orders with Machine Translation</h3><p>Counting the orders handled through machine translation gives a clear sign of how widely the service is used and its practicality. Keeping an eye on this metric helps us understand the extent of user interactions made possible by translated content, highlighting its role in making transactions smooth.</p><h3>Increased Orders</h3><p>The noticeable increase in order placements directly linked to the introduction of machine translation indicates the impact of the service on user behavior.</p><p>This metric clearly shows how translated content positively affects user engagement, leading to a boost in platform activity.</p><h3>Customer Satisfaction</h3><p>Collecting and analyzing customer feedback on the machine translation feature provides qualitative insights that helps us continue improving the experience of our users.</p><h3>Customer Retention</h3><p>Assessing changes in customer retention rates after implementation allows us to gauge the impact of the service on user loyalty.</p><h3>Future Developments</h3><p>Our goal is to expand the machine translation feature to additional stages of the customer journey, ensuring a smooth user experience across the entire platform.</p><h3>Authors:</h3><p><a href="https://medium.com/u/196ce9426e9c">Ahmad Hamouda</a>, <a href="https://medium.com/u/415b798569e5">Stefania Russo</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5d73f1efb2b3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/glovo-engineering/taste-the-world-how-our-new-machine-translation-feature-transforms-your-ordering-experience-5d73f1efb2b3">Taste the World: How Our New Machine Translation Feature Transforms Your Ordering Experience</a> was originally published in <a href="https://medium.com/glovo-engineering">The Glovo Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Cracking the Code: JS, BigInt, and the Art of Future-Proofing Your App]]></title>
            <link>https://medium.com/glovo-engineering/cracking-the-code-js-bigint-and-the-art-of-future-proofing-your-app-2ef6d00f5d0c?source=rss----9bbfe5be0af5---4</link>
            <guid isPermaLink="false">https://medium.com/p/2ef6d00f5d0c</guid>
            <category><![CDATA[nuxtjs]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[json]]></category>
            <category><![CDATA[bigint]]></category>
            <category><![CDATA[nodejs]]></category>
            <dc:creator><![CDATA[Victor Borisov]]></dc:creator>
            <pubDate>Wed, 29 Nov 2023 09:56:25 GMT</pubDate>
            <atom:updated>2023-11-29T09:56:25.214Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/680/1*WCAMdsffEhq55gA7W31-sA.png" /></figure><h3>What’s this article about?</h3><p>Ever wondered about the mysteries of JSON.parse and why it sometimes throws unexpected surprises? In the real world where we have real traffic and real users, it turns out not every platform plays nice with JSON in the same way. Parsing numbers is just the tip of the iceberg — we’ve got to tackle the quirks of both nodeJS and browsers, as well as some server-side rendering frameworks.</p><p>Join us in this exploration where we spill the beans on real-world scenarios from Glovo, sharing insights on the nitty-gritty of JSON and big numbers parsing that you might not have seen coming.</p><h3>How does JS handle numbers and what is BigInt?</h3><p>Historically, numbers in JS are represented using `number` type, which is based on the<a href="https://en.wikipedia.org/wiki/IEEE_754"> IEEE 754 floating-point standard</a>, which basically means that for every `number` used there is a 64-bit double precision number in the memory, which can safely represent any integer number between -9007199254740991 and 9007199254740991 (this is Number.MAX_SAFE_INTEGER in JS), and can even work with the floating point (although with some limitations). This should be enough to cover most of the cases, <strong>but it still has some limitations</strong>, most notably when it comes to arithmetics with the floating point and storing numbers outside of the safe numbers interval.</p><p>Due to the limitations, <em>number</em> can not safely represent big numbers, do arithmetics with them, and also has issues with arithmetics of some floating point numbers. We can use BigInt type in JS instead of the `number`, it allows only to work with integers, but it can safely work with much bigger numbers and has a pretty solid<a href="https://caniuse.com/bigint"> browser support</a>. This is all we need to know about how JS works with numbers in terms of this article, but if you’d like to dig a bit deeper into the topic,<a href="https://javascript.plainenglish.io/why-javascript-is-bad-at-math-9b8247640caa"> check this article out</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/620/0*HQOco7Oq-N2CJ_V2" /></figure><p>After reading this, you might think: “Oh, I am not planning to do any arithmetics or to work with bigger numbers, so I should not care about what you’ll say next”. And I want to assure you, we thought exactly the same until the thing that I will tell you about next happened.</p><h3>The Unexpected Twist: How Neglecting BigInt Almost Broke Our App (And why this can easily happen with you)</h3><p>Even though we use numbers for IDs of products in Glovo, we didn’t consider using BigInts because the numbers were very far from MAX_SAFE_INTEGER, and we were sure we would not reach this limit during the lifetime of the app. I’d personally prefer never using numbers and would go for strings instead (then we would have not had this problem in the first place), but the API was designed with more focus on the mobile apps at that time, and the issues we’ll be talking about here do not exist on major mobile platforms for native apps.</p><p>At some point, we had to plan for migrating one of our API services to a new API, which unifies several different applications, and it actually uses IDs that are higher than MAX_SAFE_INTEGER. We’ve immediately figured out this won’t work. Without doing any changes to our code, we set up our testing environment to use the new API to see how bad things are — and indeed they were really bad. The app had a bunch of errors, and we had a store which had all its products with IDs that are big enough to be coerced by the `number` limitations, meaning that after parsing the JSON, every product had exactly the same ID. To understand how this happens, see the screenshot below or <a href="https://github.com/vd3v/nextjs-bigint-demo-app/blob/main/app/page.tsx">check this repo with the demo app</a> from which this screenshot is.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BD3ihFFMrul82tv1" /></figure><p>The result was that, when adding 1 product on the store page, all the products of this store were added to the cart, because their (originally distinct) IDs were coerced to the same number.</p><p>This happens because there are “unsafe” big numbers in the JSON response, but native `JSON.parse` has no idea what a BigInt is and there is no way to make the native JSON.parse correctly parse these numbers from the original JSON. To make the matter worse, the RFC describing the JSON standard, recommends to use numbers inside the range of the double precision numbers (same as JS’s `number`), but it does NOT enforce any specific limit and states those limits are up to each implementation (you can see it here<a href="https://datatracker.ietf.org/doc/html/rfc8259#section-6"> https://datatracker.ietf.org/doc/html/rfc8259#section-6</a>).</p><p>So the problem here is that the service returns a technically valid JSON, but we can not parse it using built-in JS tools. This means that, if you are developing a webapp, it doesn’t matter what your FE framework/library is — there is a non-zero chance that one of the APIs you depend on may start returning unsafe numbers in the JSON, because first it’s not forbidden as per JSON RFC, second many other programming languages do not have a “default” type of the number when it comes to serialising/deserialising JSON.</p><p>For instance, your Java backend may cast longer numbers to Java `Long` data type, which will not be compatible with JS `number`, and nobody may even notice any issue until that limit is breached (e.g. IDs that are being incremented in the database). For example this code:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/eb867a5cc00c3270508bdb956f14d164/href">https://medium.com/media/eb867a5cc00c3270508bdb956f14d164/href</a></iframe><p>Will produce the following JSON (it is not possible to correctly parse it with JSON.parse, if you copy-paste it to the console you will see the result):</p><blockquote>{“id”:9223372036854775807}</blockquote><p>This can be solved by parsing JSON without using JSON.parse and handling big numbers as BigInts. There is a library for that (tm) here:<a href="https://github.com/sidorares/json-bigint"> https://github.com/sidorares/json-bigint</a>. It should cover most of the cases, but I wouldn’t be writing this article if it was the end of our struggle.</p><p>Generally, if your app is 100% client-side (SPA) and has no nodeJS server runtime, for instance something like expressJS, nuxt or next, then you should be fine with just making sure you are always using a JSON parser that can parse bigger numbers, like the one I just mentioned.</p><p>For us, it was not the end of the story. We have a customer web app with server-side-rendering (meaning we render the HTML out of VueJS components using nodeJS). It is using nodeJS and a server-side rendering framework called nuxtJS.</p><h3>Behind the Server Curtain: Tackling BigInt Issues in NodeJS SSR Apps</h3><p>On the server side, the problem is generally the same, with the main difference being that is (generally) we now want to parse much more JSONs, non-stop. In order to render (almost) any page, we need to make several HTTP requests to some of our backend services from the nodeJS (nuxtJS) app — even with HTTP caching that we have, this is still needed due to different customisations (for different users, cities, languages, stores, time of the day, etc), the experimentation which we do a lot (things like A/B tests) and of course almost real-time nature of our data (stores open and close, they edit products, start or finish promotions, etc).</p><p>When testing the application locally, everything works well with json-bigint parsing every backend API network response on the nodeJS server. By contrast in a real life scenario of a user navigating our website, their browser will also only parse a few JSON strings per minute (usually per page). In these “light” scenarios the json-bigint library works well. But in reality, each of our production nodeJS servers can be parsing tens or hundreds of JSONs every second, non-stop, 24/7. I am not entirely sure if there is a memory leak in the mentioned library or it’s just generally more memory-hungry, but the reality is that we didn’t find a way to use it on the server without causing a significant performance degradation caused by excessive memory used when enabling this library. We kept it as-is on the client, but we needed to find a way to parse JSONs with BigInts on the server, and, for our use-case it must be something less hungry in terms of memory.</p><p>We could not find any alternative solution for our case on the web, we were looking for something relatively popular and supported, slim so it won’t inflate our JS bundle, or at least something simple so that we could fork it and support. Most of the solutions are either too heavy in terms of the bundle size, or are not tested well enough and may fail on a valid JSON, <a href="https://github.com/Ivan-Korolenko/json-with-bigint/issues/3">here’s one example I found</a>. After losing all hope, we finally managed to come up with a solution that works (well, kinda, more about that later).</p><p>What we did was take the original JSON string, at first, without parsing it. First, we ran a regular expression on it, this regex will replace all the bigger numbers with a string which contains a predefined prefix and the number itself right after it. In our case it would transform this JSON:</p><blockquote>{“id”:9223372036854775807}</blockquote><p>Into something like</p><blockquote>{“id”:”APP_SERIALISED_BIGINT::9223372036854775807”}</blockquote><p>The tricky part here is the security and reliability, because manipulating JSON strings (especially with some constants that you later transform) might be dangerous. To make this approach safer we need to make sure that what we replace is actually a big number, but not something that just looks like one. For instance, if implemented poorly, there could be a number of ways to break it:</p><ul><li>If the original JSON already includes the “prefix” constant (APP_SERIALISED_BIGINT::) somewhere — this may easily break the app if there is no actual number after it. Among other risks, if the constant is placed into a JSON key, this can lead to many potential ways of allowing an attacker to manipulate the JSON structure.</li><li>Big numbers inside strings, especially if the JSON has a string property, which is another JSON — the function may start replacing values inside, which can break the JSON structure because it won’t work with escaped chars ( this behaviour is not desirable since the underlying JSON should be separately parsed with the same BigInt-enabled parser function).</li></ul><p>A well-tested regular expression should help here.</p><p>After this we can take this string and feed it into the native JSON.parse. The second argument of the .parse method is called `reviver`, it is a function which is called for every value when parsing a JSON string, it allows modifying each value found in the JSON object. The code for this would look like this:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/3ce7b31ce2dcbeef01f642d190aa9ec2/href">https://medium.com/media/3ce7b31ce2dcbeef01f642d190aa9ec2/href</a></iframe><p>With this approach we do a (relatively) cheap in terms of memory operation of replacing some things in the string and we use the native JSON.parse, which in theory should be more efficient than any JSON parser made with JS. The only problem with this approach is that, in order to properly detect where the big number is (to avoid cases mentioned above, like JSON values inside strings), we need to use a regular expression with a negative lookbehind assertion, which has a<a href="https://caniuse.com/js-regexp-lookbehind"> very limited browser support</a>, most notably it needs both macOS and iOS Safari to be of a version not less than 16.4. Luckily, it’s been supported on nodeJS since 8.10.0, so this will still work well on the server. For the browsers (mostly because of Safari) we had to still ship the custom JSON parser based on json-bigint.</p><p>The source code of the regex-based parser we created is available on github at <a href="https://github.com/vd3v/big-jason">https://github.com/vd3v/big-jason</a>, as well as the npm package at <a href="https://www.npmjs.com/package/big-jason">https://www.npmjs.com/package/big-jason</a>.</p><p>We did a performance test where we parsed a large JSON string containing different types of data, including some big numbers, using both the method with regex and the json-bigint library thousands of times. The results show that the approach with the regex consumes way less memory, around 70% less of both heap and rss memory, but it is more CPU-intensive, so it takes around 40% more time. We had no other choice but to see how much of a tradeoff would that be in production. As a result, there was no noticeable change in the memory consumption compared to what was before (where we were just losing big numbers while using the default JSON.parse), and the CPU didn’t show much change in the average load either. Before, when we were trying to use the json-bigint library in production, our servers had what we call a “slow memory leak”, where the memory would grow over the time without being cleared up, until K8S started killing those machines that ran out of memory. Under high traffic a server would not work more than a minute or two, which rendered this approach unusable for our application.</p><p>Now that we found a way to parse JSON strings with BigInt, we should be good to go, right?</p><p>Right… Not exactly. Since we are talking about an application with server side rendering, all major SSR frameworks (like next, nuxt and svelteKit) have a process called “hydration”. This is something that happens when the browser loads a page pre-rendered on nodejs, and then needs to instantiate UI components of the UI framework (like react, vue or svelte), and they need to have the same state as they had when they were rendered by the server, otherwise the user will see a page with all the data and then (once the UI library is mounted) it will become empty (it can produce much more issues, including completely breaking the app). To make it work these SSR frameworks have their ways to serialize the state, which will later be read on the client and injected into the components state before they are mounted.</p><p>As you might’ve guessed, this is exactly where the next problem with BigInts happened. Luckily, as of today this is not that much of an issue for fresh versions of both nuxt and nextjs, since they’ve released an update. They both rely on the<a href="https://github.com/Rich-Harris/devalue"> devalue</a> library, which got<a href="https://github.com/Rich-Harris/devalue/pull/32"> the support for BigInts</a> in August 2022, so there is still a chance that you’re using an outdated version without BigInt support. In our case the problem was that we were stuck with nuxt2, which relies on its own fork of devalue, which still doesn’t support BigInt. On top of that, we were using @nuxtjs/composition-api, which also handles some logic related to serialization and is using good old JSON.stringify. As a quick fix, we made a patch to those dependencies in our project with pnpm patch, but I will also open PRs to those repos with a patch to potentially help other developers struggling with BigInt on old nuxt.</p><h3>Conclusion</h3><p>With the recent patching of server-side rendering (SSR) libraries like Nuxt and Next to support BigInt, a brighter future appears on the horizon for BigInt in JavaScript. However, the true problem lies in the JSON standard itself, as there are different ways to understand and implement it in terms of parsing numbers. The lack of native support for BigInt in JSON parsing poses an ongoing challenge by forcing developers to use 3rd party solutions for such a core thing like parsing a network response. The hope is that major browsers will collectively embrace a more inclusive approach, supporting numbers of all sizes within JSON, or there will be established a new standard for JSON that would allow representing big numbers, for instance with the JS BigInt literal (for example: <em>{ “id”: 123456789123456789n}</em>). This would allow developers to use BigInts seamlessly across both web applications and on native mobile apps, not to mention cross-service communications.</p><p>I hope it was useful or interesting to you to read about our struggles. Please share your thoughts, questions, or even your own solutions in the comments. I am curious to hear how you handle BigInts in your apps (or if you are intentionally trying to avoid them) and your experience with this. Do you think this should become a part of the ECMA/JSON specification? Thank you very much for reading and have a nice day!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2ef6d00f5d0c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/glovo-engineering/cracking-the-code-js-bigint-and-the-art-of-future-proofing-your-app-2ef6d00f5d0c">Cracking the Code: JS, BigInt, and the Art of Future-Proofing Your App</a> was originally published in <a href="https://medium.com/glovo-engineering">The Glovo Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accelerate Your Android Development: Essential Tips to Minimize Gradle Build Time (Part II of II)]]></title>
            <link>https://medium.com/glovo-engineering/accelerate-your-android-development-essential-tips-to-minimize-gradle-build-time-part-ii-of-ii-b74f5d505982?source=rss----9bbfe5be0af5---4</link>
            <guid isPermaLink="false">https://medium.com/p/b74f5d505982</guid>
            <category><![CDATA[developer-experience]]></category>
            <category><![CDATA[build-time]]></category>
            <category><![CDATA[optimization]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[gradle]]></category>
            <dc:creator><![CDATA[rolgalan]]></dc:creator>
            <pubDate>Mon, 06 Nov 2023 08:52:30 GMT</pubDate>
            <atom:updated>2023-11-06T08:52:30.214Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="Long exposure picture of a highway, where the lights from the vehicles are really long continuous lines covering the road (and vehicles are actually not visible), giving a sense of really high speeds." src="https://cdn-images-1.medium.com/max/1024/0*5sxKeJAFQ4ugQo8j" /><figcaption>Photo by <a href="https://unsplash.com/@i_am_g">Guillaume Jaillet</a> on <a href="https://unsplash.com/photos/Nl-GCtizDHg?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditShareLink">Unsplash</a></figcaption></figure><h3>Introduction</h3><p><a href="https://medium.com/@rolgalan/4f35aa4a1a17">In the previous part of this article</a>, we emphasized how reducing build time can enhance developer productivity and business value.</p><p>We highlighted that caching the output of previous tasks for reusability and leveraging parallel builds are the most impactful actions.</p><p>Let’s review now some other techniques and configurations to keep improving your build times. Even if some of these might not be as effective as the ones outlined in the first article, they are still quite relevant. After you have already applied all of the previous actions, all the new ones will become quite significant to reduce even more your build time.</p><p>As it was mentioned previously, all the learnings shared here have been acquired from Android projects, but <strong>all of the Gradle techniques discussed here can be applied to any other Gradle project unrelated with mobile</strong>.</p><h3>The hardware</h3><p>While it may seem obvious, upgrading the machines that build your app should be one of your first considerations to reduce build time. This means both the remote agents from your CI/CD and your local development laptop. (Are your engineers already using M2s? 👀).</p><p>Given that building an application is a CPU and memory-intensive process, it’s crucial to understand the machines on which the project runs. <strong>Number of cores is decisive to execute tasks in parallel</strong>, as well as their clock rate to execute fast. At the same time you are going to need a lot of memory available to be able to run the whole process (specially if you parallelize). We mentioned in the previous article that it is important to parallelize; if you are investing on that, it makes sense also to make sure your machines are going to support it. In the next section we’ll discuss how this parallelization impacts also the memory.</p><p>Although often overlooked, disk I/O throughput is critical as app building involves constant disk read and write operations. We learnt this the hard way!. Quite recently we detected huge penalties during a CI agents migration to different runners, specially during the Gradle <a href="https://docs.gradle.org/current/userguide/incremental_build.html#sec:how_does_it_work">task fingerprinting</a>. The <strong>time when reusing tasks from the cache was increased by 3x when changing from Fargate to EC2 due the default disk used in the latter had worse capabilities</strong>. If you are building your projects in AWS, make sure your disk is <a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ssd-instance-store.html#nvme-ssd-volumes">NVMe</a>.</p><p>While disk space may seem trivial in today’s context and often goes unmentioned, we encountered issues when using CI agents with only 20GB of disk space (this was the limit in AWS Fargate at some point). One particular thing to look at is the transitive R class, which duplicates resources in every module from its dependencies. Currently all projects have <a href="https://developer.android.com/build/optimize-your-build#use-non-transitive-r-classes">non-transitive R class by default</a>, but if you are working with a project older than a few years, make sure to enable this flag, as it also impacts build speed.</p><h3>The JVM memory settings</h3><p>As previously mentioned, the build process demands a significant amount of memory, making <strong>memory configuration the most important setting for your project</strong>. Since Gradle executes in a JVM process, this should be done through the org.gradle.jvmargs property in the gradle.properties file.</p><p>By <a href="https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties">default</a>, Gradle is setting org.gradle.jvmargs=-Xmx512m -XX:MaxMetaspaceSize=384m, which is arguably quite small for development of any Android application nowadays.</p><p>There are many things playing together here and it’s important to take all of them into account, <strong>especially if your system is constrained and you cannot have all the RAM you would like</strong>. Let’s go one by one:</p><p><strong>The heap is the most important part</strong>, and will help you to reduce the amount spent in Garbage Collection maximizing the throughput, so make sure to set a high enough Xmx value in the org.gradle.jvmargs. At the same time, the initial heap size will also be helpful to avoid wasting some cycles dynamically increasing the heap (which requires GC to run), so you should also set up a reasonable Xms value (maybe half of your Xmx or matching it).</p><p>But you have to be careful, because the <strong>Gradle Daemon</strong> will spin up a separate process to compile the Kotlin code, the <strong>Kotlin Compiler Daemon</strong>. By default this process will inherit the <em>jvmargs</em> settings from the main Gradle Daemon, unless you add an extra kotlin.daemon.jvmargs Gradle property in the gradle.properties file. I recommend this, and you can probably limit it to a lower heap than the main Gradle daemon.</p><ul><li>Alternatively you can configure the <a href="https://kotlinlang.org/docs/gradle-compilation-and-caches.html#defining-kotlin-compiler-execution-strategy">Kotlin compiler to be executed inside the main Gradle Daemon</a>, but there might be a performance penalty. We had this for a while as our available memory in our legacy CI was quite limited and this was a good way to keep the usage under control.</li></ul><p>If you still have some Java code, Gradle will spawns separate workers for it, which used to be disposable, but since <a href="https://docs.gradle.org/8.3/release-notes.html#faster-java-compilation">Gradle 8.3</a> these are promoted to long-lived daemons. Keep an eye on these as well when configuring the memory.</p><p>Please note that, by setting any value to org.gradle.jvmargs it will override the Gradle mentioned defaults, so if you increase the heap, you will lose the existing limit to the <strong>JVM Metaspace</strong>, as the JVM doesn’t have any limit by default. At some point we had issues related to the extremely huge usage of Metaspace, which was growing uncontrollably for some unknown reason and we needed to set a maximum for it with -XX:MaxMetaspaceSize to prevent it to cannibalize our available memory, but it has not been a problem recently and we do not need this setting anymore. If you are using <a href="https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/scanners/sonarscanner-for-gradle/#troubleshooting">SonarQube</a>, it lists MetaSpace in their troubleshooting, so keep an eye on it.</p><p><strong>Unit tests</strong> also are executed in a separate JVM process, usually one <a href="https://blog.gradle.org/how-gradle-works-1">Gradle Worker</a> for each test module. By <a href="https://docs.gradle.org/current/dsl/org.gradle.api.tasks.testing.Test.html">default</a> these test workers have a maximum of 512mb for the heap (regardless of your gradle.properties settings). If you keep your modules small, this should be enough, but you can increase the value withmaxHeapSize=&quot;1024mb&quot; inside a test { } block in your Gradle script.</p><ul><li>What is important here is that <strong>all these test workers will be launched in parallel, one per core, spawning separate JVM processes that could easily occupy a big chunk of your available memory in the machine if you have many cores</strong>. In our case we are running now with 16 cores, so this easily can add up to 16 gb just for these workers in parallel (for big applications, engineers rarely run the whole suite, so this impacts mostly the CI).</li><li>Is it true that you can set the property org.gradle.workers.max to limit the amount of workers executing in parallel… but why would you do that? You are paying for all of these extra cores to maximize what you can run in parallel. So use this as the last resource.</li><li>Worth highlighting also that <strong>maxHeapSize can be configured for the whole app or overridden in a specific module</strong>. You might have some outlier that requires much extra memory that you cannot afford having in all modules (due their parallelization), so you could set extra for this specific one and leave the rest with a smaller default.</li></ul><p>And remember, <a href="https://developers.redhat.com/articles/2021/09/09/how-jvm-uses-and-allocates-memory#calculating_jvm_memory_consumption">the heap is not everything</a>. Usually heap represents around 70% of the memory used by each java process, metaspace ~20% and the rest 10% is a bunch of different areas of the native memory not really relevant for us at this point. If you have a limited amount of available memory in your machines you will need to take this into consideration when choosing your memory settings, so you ensure there is some extra memory available for all the processes.</p><h3>Update dependencies</h3><p>Even it may seem obvious to some, I frequently encounter queries in public forums from individuals struggling with outdated versions of the basic tooling. However the truth is that Gradle, JDK, AGP, Kotlin… all are constantly introducing improvements in the performance, so <strong>ensuring that your dependencies are up to date is usually a good way to keep your build times under control “for free”</strong>.</p><p>One of the latest and most relevant examples is Hilt/Dagger, the most common Dependency Injection framework in Android. This is one of the top contributors to slow builds in big projects, since it makes heavy use of annotations. It’s been based on KAPT for a long time, and it was not until some weeks ago that they <a href="https://github.com/google/dagger/issues/2349#issuecomment-1699569360">finally made the required changes to use KSP</a> instead, whose performance is way faster as it doesn’t require some intermediate steps in the middle. So… are you in the latest Dagger version already?</p><p>The best you can do is to introduce any tooling to automatically update your dependencies, such as <a href="https://github.com/renovatebot/renovate">Renovatebot</a> or <a href="https://github.com/dependabot">Dependabot</a>, which will regularly open PRs in your repos to keep updating to the latest versions and running all the CI checks.</p><h3>Other Minor optimizations</h3><p>Everything mentioned so far will introduce really noticeable improvements in your build times.</p><p>Once the major improvements are implemented, you can consider minor optimizations to further reduce build time and address edge cases. Let’s see some examples.</p><h4>Pre-cache dependencies</h4><p>Usually building a project requires several dependencies to be downloaded in the system. This might easily increase a couple of minutes (or more sometimes) your builds. Also it would be a quite erratic delay, as it will depend on the network variability. In general it is good having all or some of them accessible, so you should apply the advice about it for <a href="https://docs.gradle.org/current/userguide/dependency_resolution.html#sub:ephemeral-ci-cache">Dealing with ephemeral builds</a>.</p><p>Naturally, this extends beyond typical build dependencies to include Gradle itself, Android SDK tools, and system images required for your Robolectric tests. These should be already pre-downloaded in the CI agents running your CI.</p><p>If you use the official <a href="https://docs.gradle.org/current/userguide/github-actions.html#enable_caching_of_downloaded_artifacts">gradle-build-action with GitHub Actions</a>, this is done by default, and it even <a href="https://github.com/gradle/gradle-build-action#which-content-is-cached">caches many other elements</a> that will help to accelerate your builds even more (particularly many contents from the home ~/.gradle folder such as compiled build scripts, and more)..</p><h4>Different settings CI and local</h4><p>The structure of your CI builds, and how the tasks are influences this. In our case, we aim to validate multiple aspects in each PR, including unit tests, linting, release builds, and UI test artifacts (even if not executed). This allows us to ensure that PR changes do not impact other necessary tasks in different stages.</p><p>Since we had some powerful CI agents with many cores, we previously launched a single execution that requested all tasks simultaneously, allowing Gradle to parallelize everything with its internal workers. This has completely different memory requirements from the day to day of engineers in local builds, that usually just launches tasks one by one. For this reason we were tweaking the memory settings for the CI, overriding the jvmArgs and other gradle.properties in our CI agents.</p><p>Remember that anything declared in your home directory (~/.gradle/gradle.properties) will override the project settings, facilitating easy modification of the configuration for many of many other settings mentioned in this section.</p><h4>Avoid daemons duplication from the IDE</h4><p>If the JAVA_HOME environment variable is different from the IntelliJ IDEA (or Android Studio) JVM settings, and you usually run tasks both from the IDE and the terminal, it might duplicate the Gradle Daemon, consuming extra CPU and memory. Make sure you <a href="https://developer.android.com/build/jdks#jdk-gradle">configure both to be the same</a>.</p><p>There is a nice plugin maintained by Gradle engineers that will flag this misconfiguration and other minor optimizations. Check the <a href="https://runningcode.github.io/gradle-doctor/">Gradle Doctor plugin</a>.</p><h4>Stop watching file system in CI</h4><p>Gradle has a nice <a href="https://blog.gradle.org/introducing-file-system-watching">feature</a> that significantly accelerates incremental builds (which is enabled by default). When enabled, it allows Gradle to keep what it has learned about the file system in memory between builds instead of polling the file system on each build, reducing the amount of disk I/O needed between builds. You should have this enabled for your local development.</p><p>However, this is probably overkill for the CI as nothing is expected to change (and probably you only run a single execution), so you can disable it explicitly by adding org.gradle.vfs.watch=false to your gradle.properties. Make sure you <strong>disable this only for the CI</strong>.</p><p>We haven’t quantified the impact of this (as we applied many other changes at the time), but intuitively, this setting seems unnecessary in the CI. I would love to hear from anyone having some data around the imipact of this setting.</p><h4>Garbage collector</h4><p>Since the release of JDK 9 G1 is the default garbage collector, however Android documentation <a href="https://developer.android.com/build/optimize-your-build#experiment-with-the-jvm-parallel-garbage-collector">encourages you to use the ParallelGC</a> instead. In most cases this might not be a big difference, but in others it might be huge.</p><p>For reasons still unclear to us, some complex clean builds targettign several tasks at once resulted in GC Overhead or took over an hour with the ParallelGC, despite allocating substantial extra memory to the heap. However, we managed to reduce this to around 35 minutes simply by switching to the G1 collector.</p><p>So I am not telling you to change your garbage collector, but to <strong>encourage you to test different settings</strong>. Then, do not hesitate to try this (or other newer GCs) if you’re having memory issues, as it might help, even if the reasons why are not clear.</p><h4>Fork test execution</h4><p>I mentioned at the beginning that Gradle runs a separate parallel JVM process for each module when running the test suite. You can also <a href="https://docs.gradle.org/current/userguide/performance.html#execute_tests_in_parallel">execute the tests of the same module in paralle</a>l inside this process.</p><p>In our experience, this approach penalized our CI builds but benefited local builds, likely because CI executes everything simultaneously using all available resources, while engineers typically run tests for a single module, which is a lighter task, leaving some computer cores available.</p><h3>Experiment</h3><p>There are numerous other minor aspects that you should test and measure to ensure they suit your needs. The last few points are good examples of settings that you can evaluate to decide if they are making any improvements for you. The build process is quite complex and depends on so many different pieces that some configurations need to be tested before making a decision.</p><p>Do not hesitate to experiment with different settings in order to fine tune your build configuration and keep reducing the build time. The <a href="https://github.com/gradle/gradle-profiler">Gradle Profiler</a> is a really good tool that could help you with this.</p><h3>Conclusion</h3><p>As demonstrated, there are numerous strategies you can employ to enhance your build times. There might be many others, but these are the most relevant ones that helped the Mobile Platform team to significantly reduce the build times both locally and in the CI for the Glovo mobile apps.</p><p>The most important ones are to introduce a remote cache (specially for your CI), to ensure that the cacheability of the tasks works correctly, making sure that you leverage parallelization correctly, and having the right hardware and memory settings. Review <a href="https://medium.com/@rolgalan/common-gradle-misconceptions-03269b1a559">the first part of this article for more details about cacheability and parallelization</a>.</p><p>After addressing the major improvements, begin exploring other strategies to further reduce your build time, and conduct regular reviews to ensure your build times remain optimal.</p><p>Last, but not least, make sure to keep monitoring your build times to ensure there is no degradation over time and to quickly catch any abnormal increase due to some misconfiguration or other changes in your projects.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b74f5d505982" width="1" height="1" alt=""><hr><p><a href="https://medium.com/glovo-engineering/accelerate-your-android-development-essential-tips-to-minimize-gradle-build-time-part-ii-of-ii-b74f5d505982">Accelerate Your Android Development: Essential Tips to Minimize Gradle Build Time (Part II of II)</a> was originally published in <a href="https://medium.com/glovo-engineering">The Glovo Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to survive on-call in 4 steps]]></title>
            <link>https://medium.com/glovo-engineering/how-to-survive-on-call-in-4-steps-259361f4d1d8?source=rss----9bbfe5be0af5---4</link>
            <guid isPermaLink="false">https://medium.com/p/259361f4d1d8</guid>
            <category><![CDATA[incident-response]]></category>
            <category><![CDATA[on-call]]></category>
            <category><![CDATA[engineering-culture]]></category>
            <category><![CDATA[incident-management]]></category>
            <dc:creator><![CDATA[ema]]></dc:creator>
            <pubDate>Mon, 30 Oct 2023 09:17:14 GMT</pubDate>
            <atom:updated>2023-10-30T09:17:14.096Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/500/0*Qsbs1jCkT52QXGft" /></figure><h3>Intro</h3><p>Many tech companies have adopted the concept of “on-call”, meaning being ready to provide support in case their service is not working as expected.</p><p>But while there are well-established best practices for the monitoring/observability and incident response platforms, I think we need to focus more on the preparation and the semantics of Incident Response.</p><p>So in this article, you can find 4 very personal tips derived from my on-call experience that I hope could be useful to share!</p><h3>Breathe</h3><p><strong>First things first: Put things in perspective</strong></p><p>This depends on the domain, but in the majority of tech companies, the incident that popped up on your phone and is making your heart race is not going to put someone’s life at risk, so the first thing to do is to remind yourself that <strong>you’re not a surgeon</strong>, nobody is going to die but it’s “just” about money.</p><p>I found that thinking about this and taking a couple of deep breaths before starting to look into what happened helps reduce anxiety and be more focused, rational and effective in handling the problem.</p><p>Finally, remember also that your <strong>goal now is to mitigate, not investigate and fix it for good</strong>, so:</p><ul><li>Rollback is better than pushing a hotfix</li><li>If you suspect a specific feature could be the cause, don’t hesitate to disable it to check!</li><li>The next working day is the right time to investigate deeper</li></ul><p>If you forget this and just take your time to dive deep during an incident it’s:</p><ul><li>Bad for you ➡️ personal time lost</li><li>Bad for other on-call persons who joined to help you ➡️ time lost</li><li>Bad for the company ➡️ pay you extra to work overtime</li></ul><h3>Be prepared</h3><p>The biggest work of incident resolution can be done BEFORE the incident. Here are 4 things to include in your team practices:</p><h4>Build (and maintain) an on-call handbook</h4><p>Have a shared team handbook for on-call</p><ul><li>Need to be the index, the source of truth of incident management</li><li>Need to be useful ➡️ links to the dashboard, links to logs, toggles, …</li><li>Need to be SHORT, FAST ➡️ you won’t have time to read</li><li>Need to be shared and maintained by the team</li></ul><h4>Write SOPs</h4><p>Every time you solve a problem that:</p><ul><li>Require you some work, like coding a script or crafting an API request</li><li>Is not extremely specific, so could be useful again</li></ul><p>It’s time to create a Standard Operating Procedure!</p><p>This basically means the next time that during an incident there is a situation, before jumping into crafting a solution you take a look and find an SOP, then just follow the steps.</p><p><strong>Example</strong>: An event consumer stopped working due to a bug and some critical events got lost. To solve the issues, the on-call engineers will probably do something like:</p><ul><li>Move the offset of the consumer group, if messages are still in the broker</li><li>Force the sender to resend the messages</li><li>Manually sync data reading from the sender DB</li></ul><p>Even though the problem was different, it happened another time that this service was out-of-sync, so you find an SOP and just follow the steps: after a few minutes, the problem is fixed.</p><p>These tasks are not straightforward and could even make the situation worse, plus it is much harder to think straight with anxiety, maybe after being woken up in the middle of your sleep. Imagine how easier it would be to find a step-by-step guide at that moment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/568/0*91SwGmQW98jUxRrn" /></figure><p>To Recap: <strong>common problems have common solutions</strong></p><ul><li>Hard to think straight during an incident</li><li>Can immensely reduce mitigation time</li><li>Do your homework (the day after, not during the incident)</li></ul><h4>Roleplaying</h4><p>Take time to train (especially new joiners) on realistic scenarios!</p><p>An experienced engineer who has already managed many incidents can raise a fake incident, and then pretend to be a RTO agent that can provide details on the problem. The trainees will then need to go through logs, metrics and apply resolutions, while the expert engineer “shadows” them.</p><p>If you ever played D&amp;D, this is what I am talking about, I found this teaching technique to be crazily more effective than any others.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*Onpn5pO-equuO6kP" /></figure><h4>Prioritize OPS tickets</h4><p>This point is mainly for engineering managers or lead engineers: you need to make sure that critical ops tickets get prioritized or they will never be done.</p><p>By this, I don’t mean forcefully push them in the sprint, but have a conversation with your PM/Business team and kindly explain to them why this is important and what are the business implications of NOT doing it.</p><p>If you explained it correctly, I am sure that they will be the first willing to push them.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/717/0*a6e2ia7qwtNXHz--" /></figure><h3>Team Power</h3><p>This point seems very straightforward, and yet I’ve seen so many times people not doing it!</p><p><strong>Call for help</strong>: the on-call engineers are a team, don’t try to solve the problem on your own if another service is involved, e.g. if there’s an infrastructure problem and you’re not an SRE: don’t wait! Call one!</p><p>The mitigation time can be hugely reduced and they are on-call, so you should be expected to be called <strong>not only for a problem on your specific service</strong> but for anything in the company you could be helpful solving.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/0*8Vadfeaw6ANV3zSc" /></figure><p>Of course the same will apply to you, so <strong>help others if you want to be helped</strong>, join on-calls of others and actively help until it’s solved.</p><h4>About guilt</h4><p>It’s very important to set a blame-free culture in your company, not only because it’s the right thing but also because it’s more effective: people will have less anxiety and will focus more on the learnings of an error rather than the blaming.</p><p>Also, even if it was you who wrote a bug that is destroying prod, <strong>it should not be that easy to compromise a company’s service</strong>, there should be processes put in place by engineering leaders like code reviews, automatic rollbacks, automatic migrations checks, … everything that is commonly defined as “Guardrails” (ref The Staff Engineer’s Path — Tanya Reilly).</p><h3>If everything else failed</h3><p>A couple of suggestions to be put in your handbook, for when you encounter a difficult problem not easy to debug:</p><ul><li>Check recent deployments (<strong>not only of your service</strong>)</li><li>Check recent feature toggle changes (<strong>not only of your service</strong>)</li><li>Try to focus on a specific case, even if millions of orders/users have errors, <strong>focus on one</strong> and deeply debug it</li><li>Call for help: call other teams, call all your team, call your manager</li><li>Talk more with RTO agents:<br>- Ask for more cases or if they can spot some pattern in the problem<br>- Consider a manual solution, sometimes it’s faster than coding a script<br>- Ask them for possible mitigation like closing the service or sending a message to the customers, you’re less prone to customer churn if you’re honest and declare that you have a problem and working on it</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/498/0*G7VCsdo1NBTd-B8_" /></figure><h3>Recap</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/291/1*KsPqPfoiFnbmP4Nm-EpqHQ.png" /></figure><h4>DON’T</h4><ul><li>Try to fix the problem for good.</li><li>Hesitate to call other teams.</li><li>Blame or fear to be blamed, every incident is everyone’s fault.</li><li>Even if you wrote a bug, it should not be that easy to compromise a company’s service.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/333/1*7xeGRW5g95iyy_PTnFCxAQ.png" /></figure><h4>DO</h4><ul><li>Do your homework: Handbook, SOPs, Prioritize Ops tickets, Training.</li><li>Go for the fastest way to mitigate.</li><li>Call for help, even if it’s your ownership.</li><li>Talk and coordinate with the RTO.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=259361f4d1d8" width="1" height="1" alt=""><hr><p><a href="https://medium.com/glovo-engineering/how-to-survive-on-call-in-4-steps-259361f4d1d8">How to survive on-call in 4 steps</a> was originally published in <a href="https://medium.com/glovo-engineering">The Glovo Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Accelerate Your Android Development: Top Techniques to Reduce Gradle Build Time (Part I of II)]]></title>
            <link>https://medium.com/glovo-engineering/accelerate-your-android-development-top-techniques-to-reduce-gradle-build-time-part-i-of-ii-4f35aa4a1a17?source=rss----9bbfe5be0af5---4</link>
            <guid isPermaLink="false">https://medium.com/p/4f35aa4a1a17</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[gradle]]></category>
            <category><![CDATA[optimization]]></category>
            <category><![CDATA[developer-experience]]></category>
            <category><![CDATA[build-time]]></category>
            <dc:creator><![CDATA[rolgalan]]></dc:creator>
            <pubDate>Mon, 23 Oct 2023 09:11:37 GMT</pubDate>
            <atom:updated>2024-01-02T11:44:24.327Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="Super sonic plane, breaking the sound barrier, with the distinctive white cloud in the tail of the aircraft that happens when at that high speed." src="https://cdn-images-1.medium.com/max/1024/0*UDmTD5X42PWU_IaU" /><figcaption><a href="https://commons.wikimedia.org/wiki/File:FA-18_going_transonic.JPG">Realbigtaco, CC BY-SA 3.0</a> (flipped)</figcaption></figure><h3>Introduction</h3><p>Everyone wants faster builds, don’t they? <strong>Reducing build time is one of the most important actions you can take to improve the Developer Experience, as it reduces the feedback loop and helps the engineers to iterate faster</strong>, reducing their idle time and allowing your team to keep focused on delivering new features, adding business value to your app.</p><p>Accelerating the build process helps your developers in two ways: first while working locally they can compile and execute tests faster; and second for the CI checks to pass, so they don’t need to wait for a long times before PRs are mergeable (besides review times, of course, but that’s a human problem that won’t be solved by this). Both are important, and good news: most of the techniques to get faster build time are impacting positively both local and CI builds.</p><p>Over a year ago, our CI checks executed in the Pull Requests took around 1 hour of time. By analyzing our builds and applying many different improvements, we managed to reduce them to around 15 min in average.</p><p>Gradle offers nice documentation to <a href="https://docs.gradle.org/current/userguide/performance.html">improve the performance of the builds</a>, and also Android documentation provides some good tips to <a href="https://developer.android.com/build/optimize-your-build">optimize your build speed</a>. However <strong>most of these recommendations are not as straightforward as enabling a flag or changing a value. </strong>In this series of articles we’ll <strong>explain with more details the most important ones and how to leverage them</strong>, but also go beyond them and highlight other techniques and findings that helped Glovo to<strong> </strong>reduce the build times by 75%.</p><p>This article is divided into two posts: In this first one we will discuss the most impactful options that you can enable to cut your build times significantly. In the next one, we will talk about other less impactful actions, but still quite relevant and highly recommended to apply after you have done the first ones.</p><p>Even if all learnings are extracted from a long journey optimizing Android projects, <strong>all of the Gradle techniques discussed here can be applied to any other Gradle project unrelated with mobile</strong>.</p><h3>Parallel tasks</h3><p>This is principles of computation 101: executing multiple tasks simultaneously rather than sequentially will expedite the entire process.</p><p>It really helps to make the build process faster, as many tasks can be executed in parallel, reducing the time significantly. <strong>Some of our projects are built in around 6 min, whereas in serial they would take around 30 min</strong>.</p><p>This is disabled by default in Gradle. You should add org.gradle.parallel=true to your properties file.</p><p>This flag will allow Gradle to build independent subprojects in parallel. <strong>Projects</strong> are the Gradle terminology for what we usually refer to as <strong>modules</strong>. Fortunately <a href="https://developer.android.com/topic/modularization">modularizing your application</a> is part of the modern Android development practices, so I am not going to get into details about it. However, these two topics are highly connected.</p><p>Please do not think that enabling this flag, and having modules, is all that is required, as this is not true. The reality is that <strong>you need to build your modules architecture in a conscious way to leverage all the parallelism</strong> that Gradle can provide. This is so because each module depends on other modules, and they cannot start building until the ones they depend on are completed.</p><p>For this reason you should <a href="https://www.droidcon.com/2022/11/15/modularization-flatten-your-graph-and-get-the-real-benefits/">flatten your modules graph</a>, reducing the height and the cross dependencies. Once you get this, is when you can get the real benefit of parallel tasks execution.</p><p>Besides the architecture, parallelization will be constrained by the hardware: not only the number of cores, but also the memory available for the system. We will review the hardware configuration in detail in the following article.</p><p>Note that some parallelization, at different levels, might still happen even without this flag disabled, see <a href="https://medium.com/@rolgalan/common-gradle-misconceptions-03269b1a559a">Common Gradle misconceptions</a> to learn more..</p><h3>Cacheability</h3><p>Another fundamental concept from computation is to reuse previously executed tasks.</p><p>The fundamental part of Gradle to improve the performance of the build system is <em>“the ability to avoid doing work that has already been done”</em>. Gradle bases this in the concept of <a href="https://docs.gradle.org/current/userguide/incremental_build.html">incremental builds</a> and the <a href="https://docs.gradle.org/current/userguide/build_cache.html">build cache</a>.</p><p>Both are interconnected as they are based on the same principle of fingerprinting each task inputs and storing the task outputs. The only difference is that <strong>incremental builds live in the project scope</strong> (if the output exists in the build folder, so it doesn’t survive a clean), whereas <strong>the cache is persisted somewhere else</strong>, allowing you to recycle the tasks outputs of previously executed tasks even if you cleanup the project completely (as long as their inputs don’t change).</p><p><strong>Build Cache itself has two levels: local and remote</strong>. Local caches are usually stored in your home directory (typically your ~/.gradle/caches unless you <a href="https://docs.gradle.org/current/userguide/build_cache.html#sec:build_cache_configure_local">declared it</a> somewhere else). This really helps for local development, but for most CI executions this will barely help, especially if you are using ephemeral agents, which is usually the most common case (unless you have some shared “local” disk).</p><p>Incremental builds work out of the box in Gradle. In order to enable (<strong>local</strong>) caching just add org.gradle.caching=true in your gradle.properties.</p><p>This is good already, but what really pays off is the <a href="https://docs.gradle.org/current/userguide/build_cache.html#sec:build_cache_configure_remote">remote cache</a>, which allows caching tasks from a different machine, so if you have ephemeral builds this is a great way to reduce the execution time by reusing what was already built in previous jobs. Engineers running local builds can also get the benefit of this remote cache (specially when changing branches or fetching changes from the upstream repository). Among all the techniques described in these articles, remote cache was by far the most successful one for us,<strong> helping us to cut our build times in the CI by half</strong> when it was introduced.</p><p>In order to do this, you will need to maintain a remote Gradle build cache node somewhere, and declare it in your Gradle build files. You can either run your own <a href="https://hub.docker.com/r/gradle/build-cache-node/">remote Gradle build cache</a> (we had this for a while through our own internal Artifactory instance that we use for internal dependencies) or buy some of the services that are offering it (we currently get it from Develocity, among other features).</p><p><strong>The cache node needs to be configured to have enough disk to storage to artifacts generated/used by your organization,</strong> at least in the last 24 hours. Otherwise you won’t leverage this completely, as “old” cache entries will be evicted too soon, getting a higher misses-rate than you should have. When we first introduced the remote cache, we didn’t notice that it was using only 10 gb of space by default. When increased it to 100gb we increased the remote hit-rate from 82% to 96%, with a ~20% build time reduction in all our projects.</p><figure><img alt="Graph of build time showing a more or less stable line of around 28–30 min median time, going down to around 18min after the mentioned changes increasing the remote node disk storage were applied." src="https://cdn-images-1.medium.com/max/1024/1*cqk2kJkBD-BiDRbi3br6YQ.png" /></figure><p>Please note that <strong>modularization also plays an important role in terms of cacheability as well</strong>, because Gradle’s basic building blocks for cacheability tasks are the modules. So the more you modularize, the more chances you will have to reuse code, and the faster builds you will have.</p><p>Also make sure you do an adequate usage of <a href="https://medium.com/mindorks/implementation-vs-api-in-gradle-3-0-494c817a6fa">api vs implementation</a> (<strong>as rule of thumb: always use</strong> <strong>implementation</strong>) to ensure you don’t invalidate the cache unnecessarily: <em>implementation</em> only requires recompiling the modules depending on the changed module, whereas <em>api</em> will invalidate also those depending on the parent.</p><h3>Optimizing Cacheability</h3><p>Similar to the case of parallelization, one might assume that simply enabling the cache and setting up a remote cache node would suffice. However the devil is in the details and cacheability is not trivial at all: <strong>tasks need to be carefully designed with cacheability in mind and there might be many reasons why it gets invalidated</strong>.</p><p>Even if you don’t create many tasks yourself, you might still be impacted by third party tasks added to your build. In those cases you might not be able to fix the issue, but if you detect it you should have enough data to report this to the library authors (or propose a fix yourself if it’s an open source project).</p><p>One of the most common reasons to fail is that some tasks requiring files as their input declare them as absolute paths, making them mutually incompatible. Usually there is not much you can do when you face these cases other than reporting to the original author hoping they fix it fast.</p><p>One minor optimization you can do here is verify that your CI agents are always using the same path for the project. This might sound crazy, but Jenkins by default loads the project in a folder with the name of the branch or the PR number. You can use a specific <a href="https://www.jenkins.io/doc/pipeline/steps/workflow-durable-task-step/#ws-allocate-workspace">fixed workspace with the </a><a href="https://www.jenkins.io/doc/pipeline/steps/workflow-durable-task-step/#ws-allocate-workspace">ws command of the Pipeline Step</a>.</p><p>Fortunately most of the common libraries used for Android development are currently properly implemented with relative inputs (thanks to all the people constantly watching this and reporting to the library owners), but it is good to keep this in mind when analyzing the reasons for failed cache tasks.</p><p>Besides the file paths, there are many other reasons for the tasks to fail caching, and you will need to keep reviewing your builds, <a href="https://docs.gradle.org/current/userguide/build_cache_debugging.html">debugging and diagnosing cache misses</a>.</p><p>In my experience, some of the other most common reasons for Android are those introducing dynamic values in the Manifest or the BuildConfig files, as these files are the input for many other tasks down the line, so introducing any variability in those would invalidate all the subsequent tasks. For example:.</p><ul><li><em>Version name/code</em>. Some projects automatically increase the version code for each commit, which will easily invalidate most of your tasks between executions.</li><li><em>BuildConfig values</em>. You might be getting the commit hash for some verification/tracking purposes. Or updating any other value from the BuildConfig in a dynamic way.</li></ul><p>It is quite possible that you cannot completely get rid of these values in all of your flavors, but <strong>you should ensure you use fixed values in your CI and in your development flavor for all the above examples and any other dynamic property</strong> that could change frequently during development.</p><p>However, failures are not the sole issue. It’s crucial to evaluate the effectiveness of any caching mechanism used. Why? Because storing and retrieving cache items introduces some overhead. Some tasks are so simple that caching them introduces negative savings, so it’s better to always run them. You can override any task behavior by setting specific conditions in task.outputs.doNotCacheIf(). There is a <a href="https://github.com/gradle/android-cache-fix-gradle-plugin">really good plugin</a>, maintained by Gradle engineers, that automatically disables the most common Android tasks known to have negative savings.</p><p>In general you could get a lot of insights from your builds from the free <a href="https://scans.gradle.com/">Gradle build scans</a>. However, if you want to go deeper you would need to use the paid <a href="https://gradle.com/">Develocity</a> (formerly Gradle Enterprise), which allows deeper analysis (particularly comparing two build scans to review what was the change in the inputs that triggered the task rerun instead of reusing it from the cache).</p><p>I would also encourage you to run the <a href="https://github.com/gradle/gradle-enterprise-build-validation-scripts">Gradle build validation scripts</a> regularly to get a complete overview of your tasks’ cacheability and detect any regressions quickly.</p><h3>Configuration Cache</h3><p>You might be wondering if this is about configuring the cache… However, Configuration Cache is totally unrelated to Cache Configuration 😅.</p><p>In order to execute your task, Gradle needs to create a tasks graph to know what the dependencies of your project are, by evaluating your build scripts. Depending on your structure, how many plugins you have, and how big your project is, this process could be time-consuming.</p><p>Fortunately Gradle now can <a href="https://docs.gradle.org/current/userguide/configuration_cache.html">cache the outputs from this Configuration phase</a> allowing us to reuse it and get some time back in the next builds. In general we had this enabled only for local builds, as due to the ephemeral nature of the CI builds it is most probably not worth wasting time caching this. You can do this easily by adding the following line to the gradle.properties file org.gradle.configuration-cache=true.</p><p>As with the previous cases, this is not as easy as enabling the flag. What a surprise, huh?</p><p>Initially, this necessitates adherence to really strict rules for your Gradle scripts, which you may not have been implementing (unless you’ve been keeping up with the latest Gradle best practices). Depending on how many customizations you have in Gradle you would need to invest a significant amount of time rewriting some tasks to follow the new rules that ensure that all tasks are really independent of each other and do not rely on “global” inputs that might change due to side effects.</p><p>With that you would be able to start using Configuration Cache. However, this might still not be enough. This one is funny, because usually we just care about Configuration Cache rules to not be violated, however it might happen that the way we define the tasks, makes them to be frequently invalidated in any case. Not leveraging this feature at all.</p><p>A good example of this happened to us quite recently. One of our custom tasks was ensuring that some lints run only in the modified code, so it was using as an input the result of git status. Therefore, anytime <strong>a new file</strong> was modified, conf cache got invalidated. Same happens if you are reading the head hash (maybe you want to keep it for some verification/tracking), or if you are counting the commits (for example to automate the versionCode). In all of these examples you will see some warning like:</p><p>Calculating task graph as configuration cache cannot be reused because output of the external process ‘git’ has changed.</p><p>There is another advantage of enabling this flag. This was one of the most surprising things that I learnt recently: Configuration Cache also <a href="https://docs.gradle.org/current/userguide/configuration_cache.html#config_cache:intro:performance_improvements">allows tasks parallelization inside each module</a>! <strong>Without Configuration Cache, even if your module has some independent tasks, they will always run serially</strong>.</p><p>I am not familiar with the Gradle internals, but Configuration Cache requires really strict rules to avoid access to the project settings during execution, so my guess is that this “safeguard” also guarantees that independent tasks are not changing any setting that would be required for other tasks, allowing Gradle to run them in parallel.</p><h3>Conclusion</h3><p>In this article, we’ve outlined the most effective strategies to significantly decrease your Gradle build times.</p><p>If you think about it, it’s all based on a few basic, yet powerful concepts: reusing what has already been done, and executing several tasks at the same time.</p><p>The beauty of this is that it all combines together: proper modularization allows better parallelization and caching/reusing more tasks; when you reuse most of your tasks, you have more cores available, so you have idle resources to parallelize at module level.</p><p>Even if the underlying concepts might be quite common,<strong> optimizing them requires constant dedication and a good understanding of their fundamentals</strong>. For this reason in Glovo we have a Mobile Platform team, monitoring and ensuring fast builds for the mobile projects both in the CI and locally; this way the product engineers can focus on delivering business value fast, with quick feedback loops, without worrying about their build time.</p><p>In a few days we will share a second part, reviewing some other important techniques and settings to ensure you keep your build times low, helping to speed-up the build duration. Stay tuned!</p><p><strong>[UPDATE]</strong> Continue reading the second part in <a href="https://medium.com/glovo-engineering/accelerate-your-android-development-essential-tips-to-minimize-gradle-build-time-part-ii-of-ii-b74f5d505982">Accelerate Your Android Development: Essential Tips to Minimize Gradle Build Time (Part II of II)</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4f35aa4a1a17" width="1" height="1" alt=""><hr><p><a href="https://medium.com/glovo-engineering/accelerate-your-android-development-top-techniques-to-reduce-gradle-build-time-part-i-of-ii-4f35aa4a1a17">Accelerate Your Android Development: Top Techniques to Reduce Gradle Build Time (Part I of II)</a> was originally published in <a href="https://medium.com/glovo-engineering">The Glovo Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>