<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Super Payments</title>
    <description>The latest articles on DEV Community by Super Payments (@superpayments).</description>
    <link>https://dev.to/superpayments</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F8622%2F812d8f21-e0e1-4f69-bb76-cfe5e464cb8c.png</url>
      <title>DEV Community: Super Payments</title>
      <link>https://dev.to/superpayments</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/superpayments"/>
    <language>en</language>
    <item>
      <title>Setting Up a Robust Local DevX for Snowflake Python Development</title>
      <dc:creator>Jag Thind</dc:creator>
      <pubDate>Fri, 27 Feb 2026 17:04:28 +0000</pubDate>
      <link>https://dev.to/superpayments/setting-up-a-robust-local-devx-for-snowflake-python-development-12pb</link>
      <guid>https://dev.to/superpayments/setting-up-a-robust-local-devx-for-snowflake-python-development-12pb</guid>
      <description>&lt;p&gt;In the evolving world of data engineering, developing Python-based workloads in Snowflake (via &lt;a href="https://docs.snowflake.com/en/developer-guide/snowpark/index" rel="noopener noreferrer"&gt;Snowpark&lt;/a&gt;, Python UDFs, or Stored Procedures) has become increasingly popular. However, as pipelines become more complex, a critical question arises: &lt;strong&gt;How should we develop and maintain our Python code for Snowflake?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While the convenience of browser-based editors like Snowflake Workspaces is fine for quick scripts, there is a significant "Developer Experience (DevX) Gap" that emerges when you try to build production-grade Python code in a browser tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm writing this blog
&lt;/h2&gt;

&lt;p&gt;I've seen many Data Engineers and Analytics Engineers fall into the "UI Trap" of writing complex Python logic directly in Snowflake, only to struggle with inconsistent environments, broken dependencies, and the frustration of "it works on my machine, but not on others" problems. This blog is born out of a desire to share a better way.&lt;/p&gt;

&lt;p&gt;My goal is to encourage people to step out of the browser and into a professional local development environment. By establishing repeatable local dev environments where every developer uses the same Python version, the same dependencies, and the same tooling, we can build Python-based features that are not just functional and robust, but most importantly maintainable by others.&lt;/p&gt;

&lt;p&gt;One aspect of democratizing data rich features in a product is by making it easier to develop and maintain code using consistent tools. This is why we need to focus on local DevX!&lt;/p&gt;

&lt;h3&gt;
  
  
  What we'll cover
&lt;/h3&gt;

&lt;p&gt;We will explore the merits of a local-first approach to Snowflake Python development, specifically focusing on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic Python versions&lt;/strong&gt; with &lt;code&gt;pyenv&lt;/code&gt; and &lt;code&gt;.python-version&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robust dependency management&lt;/strong&gt; with &lt;code&gt;Poetry&lt;/code&gt; and &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent tooling&lt;/strong&gt; configured in a single file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplified task execution&lt;/strong&gt; with &lt;code&gt;Poe the Poet&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Python version management with pyenv
&lt;/h2&gt;

&lt;p&gt;On macOS &lt;a href="https://github.com/pyenv/pyenv" rel="noopener noreferrer"&gt;pyenv&lt;/a&gt; is a tool for managing multiple Python versions. It allows you to install and switch between different Python versions on a per-project basis by creating a &lt;code&gt;.python-version&lt;/code&gt; file in the project root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters for DevX:&lt;/strong&gt; By pinning the Python version in version control, you ensure that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every developer uses the same Python version for the project.&lt;/li&gt;
&lt;li&gt;Your CI/CD pipeline can install the exact same version.&lt;/li&gt;
&lt;li&gt;You avoid subtle bugs that arise from Python version differences.&lt;/li&gt;
&lt;li&gt;Dependencies work consistently (some packages require specific Python versions).&lt;/li&gt;
&lt;li&gt;Debugging is easier when issues are reproducible across all environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install &lt;code&gt;pyenv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;pyenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a &lt;code&gt;.python-version&lt;/code&gt; file in the project root:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3.10
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install the Python version specified in the &lt;code&gt;.python-version&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pyenv &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Verify the desired Python version is installed and is set for the project:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pyenv version
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Dependency management with Poetry and the &lt;code&gt;pyproject.toml&lt;/code&gt; file
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://python-poetry.org/docs" rel="noopener noreferrer"&gt;Poetry&lt;/a&gt; is a tool for dependency management and packaging in Python. It allows you to declare the packages your project depends on and it will manage (install/update) them for you. Poetry offers a lockfile to ensure repeatable installs, and can build your project for distribution.&lt;/p&gt;

&lt;p&gt;It uses the &lt;code&gt;pyproject.toml&lt;/code&gt; file (which we'll explore next) as its source of truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters for DevX:&lt;/strong&gt; With &lt;code&gt;pyproject.toml&lt;/code&gt; and Poetry, you've eliminated the "works on my machine, but not on others" problem at the dependency level. Every developer and every CI/CD runner will install the exact same versions of every package, every time!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Installing Poetry&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install Poetry using Homebrew:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;poetry
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Verify the installation:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poetry &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure Poetry to create virtual environments in the project directory (recommended for better DevX). This ensures that when you run &lt;code&gt;poetry install&lt;/code&gt;, it creates a &lt;code&gt;.venv&lt;/code&gt; folder directly in the project, making it easy to activate and manage:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poetry config virtualenvs.in-project &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The &lt;code&gt;pyproject.toml&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;pyproject.toml&lt;/code&gt; file is a &lt;a href="https://peps.python.org/pep-0518/" rel="noopener noreferrer"&gt;PEP 518&lt;/a&gt; standard that replaces the need for multiple configuration files (&lt;code&gt;setup.py&lt;/code&gt;, &lt;code&gt;requirements.txt&lt;/code&gt;, &lt;code&gt;setup.cfg&lt;/code&gt;, etc.) with one unified file. It uses &lt;a href="https://toml.io/en/" rel="noopener noreferrer"&gt;TOML (Tom's Obvious, Minimal Language)&lt;/a&gt; format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Single source of truth:&lt;/strong&gt; All project configuration lives in one file.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Version constraints:&lt;/strong&gt; You can specify package versions according to Poetry's &lt;a href="https://python-poetry.org/docs/dependency-specification/#version-constraints" rel="noopener noreferrer"&gt;dependency specification and version constraints&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Deterministic builds:&lt;/strong&gt; Poetry generates a &lt;code&gt;poetry.lock&lt;/code&gt; file that pins every dependency—both direct (what you specify) and transitive (dependencies of your dependencies)—ensuring identical installs across environments.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Tool configuration:&lt;/strong&gt; You can configure multiple tools in the same file (no need for separate config files).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Example &lt;code&gt;pyproject.toml&lt;/code&gt; file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PROJECT_NAME"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PROJECT_DESCRIPTION"&lt;/span&gt;
&lt;span class="py"&gt;authors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;{name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"YOUR_NAME",email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"youremail@domain.com"&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;readme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"README.md"&lt;/span&gt;

&lt;span class="c"&gt;# Production dependencies that your code needs to run.&lt;/span&gt;
&lt;span class="nn"&gt;[tool.poetry.dependencies]&lt;/span&gt;
&lt;span class="py"&gt;python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mf"&gt;3.11&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;snowflake-snowpark-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.33.0"&lt;/span&gt; &lt;span class="c"&gt;# Snowflake Snowpark Python library&lt;/span&gt;
&lt;span class="py"&gt;pydantic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2.11.7"&lt;/span&gt;                  &lt;span class="c"&gt;# Data validation library in Python&lt;/span&gt;

&lt;span class="c"&gt;# Development-only tools that aren't needed in production.&lt;/span&gt;
&lt;span class="nn"&gt;[tool.poetry.group.dev.dependencies]&lt;/span&gt;
&lt;span class="py"&gt;black&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^23.0.0"&lt;/span&gt;           &lt;span class="c"&gt;# Code formatter&lt;/span&gt;
&lt;span class="py"&gt;pylint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^3.0.0"&lt;/span&gt;           &lt;span class="c"&gt;# Linter for code quality&lt;/span&gt;
&lt;span class="py"&gt;isort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^5.13.2"&lt;/span&gt;           &lt;span class="c"&gt;# Import statement organiser&lt;/span&gt;
&lt;span class="py"&gt;poethepoet&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^0.27.0"&lt;/span&gt;      &lt;span class="c"&gt;# Task runner for simplifying development tasks&lt;/span&gt;
&lt;span class="py"&gt;pytest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^8.1.2"&lt;/span&gt;           &lt;span class="c"&gt;# Testing framework for Python&lt;/span&gt;
&lt;span class="py"&gt;pytest-xdist&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^3.0.0"&lt;/span&gt;     &lt;span class="c"&gt;# Run tests in parallel for faster execution&lt;/span&gt;
&lt;span class="py"&gt;pytest-cov&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"^5.0.0"&lt;/span&gt;       &lt;span class="c"&gt;# Generate code coverage reports&lt;/span&gt;

&lt;span class="nn"&gt;[build-system]&lt;/span&gt;
&lt;span class="py"&gt;requires&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;["poetry-core&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;build-backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"poetry.core.masonry.api"&lt;/span&gt;

&lt;span class="c"&gt;# Configure all your tools&lt;/span&gt;

&lt;span class="nn"&gt;[tool.black]&lt;/span&gt;
&lt;span class="py"&gt;line-length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
&lt;span class="py"&gt;target-version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'py310'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[tool.isort]&lt;/span&gt;
&lt;span class="py"&gt;profile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"black"&lt;/span&gt;
&lt;span class="py"&gt;multi_line_output&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="nn"&gt;[tool.pylint]&lt;/span&gt;
&lt;span class="py"&gt;max-line-length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
&lt;span class="py"&gt;fail-under&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;9.5&lt;/span&gt;

&lt;span class="c"&gt;# Configure tasks for Poe the Poet&lt;/span&gt;
&lt;span class="nn"&gt;[tool.poe.tasks]&lt;/span&gt;
&lt;span class="c"&gt;# Private tasks (prefixed with _ to hide from the help menu)&lt;/span&gt;
&lt;span class="py"&gt;_format_black&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"black ."&lt;/span&gt;
&lt;span class="py"&gt;_format_isort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"isort ."&lt;/span&gt;
&lt;span class="py"&gt;_pylint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"pylint src/"&lt;/span&gt;

&lt;span class="c"&gt;# Public tasks that compose the individual tools&lt;/span&gt;
&lt;span class="py"&gt;format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"_format_black"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"_format_isort"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;lint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"_pylint"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;test&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"pytest --cov -vv"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Installing dependencies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once your &lt;code&gt;pyproject.toml&lt;/code&gt; is set up, installing all dependencies (including dev dependencies) is a single command. It will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a virtual environment (in &lt;code&gt;.venv&lt;/code&gt; if you configured Poetry to do so).&lt;/li&gt;
&lt;li&gt;Install all dependencies (including dev dependencies).&lt;/li&gt;
&lt;li&gt;Generate or update &lt;code&gt;poetry.lock&lt;/code&gt; to ensure reproducible installs across environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For new projects where you haven't written code yet, you'll need to use the &lt;code&gt;--no-root&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;poetry &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-root&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;--no-root&lt;/code&gt; is needed initially:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you first create a project manually or with &lt;code&gt;poetry init&lt;/code&gt;, Poetry assumes you're building a package. If you run &lt;code&gt;poetry install&lt;/code&gt; without any code, you'll get an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Installing the current project: example-project (0.1.0)
Error: The current project could not be installed: No file/folder found for package example-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--no-root&lt;/code&gt; flag tells Poetry to skip installing your project as a package and only install the dependencies you've specified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you won't need &lt;code&gt;--no-root&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you've written code and added a &lt;code&gt;packages&lt;/code&gt; section to your &lt;code&gt;pyproject.toml&lt;/code&gt; file like the example below, you can use the standard &lt;code&gt;poetry install&lt;/code&gt; command (without &lt;code&gt;--no-root&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.poetry]&lt;/span&gt;
&lt;span class="py"&gt;packages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;[{include&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;YOUR_PACKAGE_NAME&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;from&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"src"&lt;/span&gt;&lt;span class="err"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuring VS Code (optional):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To use the project's virtual environment in VS Code / Cursor for IntelliSense, debugging, and running code in the IDE:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Press &lt;code&gt;Cmd+Shift+P&lt;/code&gt; (or &lt;code&gt;Ctrl+Shift+P&lt;/code&gt; on Windows/Linux)&lt;/li&gt;
&lt;li&gt;Type "Python: Select Interpreter"&lt;/li&gt;
&lt;li&gt;Select "Enter interpreter path"&lt;/li&gt;
&lt;li&gt;Enter the path to your project's virtual environment: &lt;code&gt;./&amp;lt;PROJECT_ROOT&amp;gt;/.venv/bin/python&lt;/code&gt; (adjust the path to match your project location)&lt;/li&gt;
&lt;li&gt;VS Code will now use the same Python environment as Poetry, giving you access to all installed packages and proper code completion.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Poe the Poet: Simplifying development tasks
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://poethepoet.natn.io/" rel="noopener noreferrer"&gt;Poe the Poet&lt;/a&gt; is a task runner that lets you define common development commands in your &lt;code&gt;pyproject.toml&lt;/code&gt; file. Instead of remembering long commands like &lt;code&gt;poetry run black . &amp;amp;&amp;amp; poetry run isort . &amp;amp;&amp;amp; poetry run pylint src/&lt;/code&gt;, you can create a simple alias and run &lt;code&gt;poetry run poe lint&lt;/code&gt;. See the &lt;code&gt;[tool.poe.tasks]&lt;/code&gt; section in the example &lt;code&gt;pyproject.toml&lt;/code&gt; file above for the configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consistency:&lt;/strong&gt; Everyone on your team uses the same commands&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity:&lt;/strong&gt; &lt;code&gt;poe lint&lt;/code&gt; instead of remembering multiple flags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composability:&lt;/strong&gt; Chain tasks together (e.g., &lt;code&gt;lint&lt;/code&gt; runs &lt;code&gt;format&lt;/code&gt; then &lt;code&gt;pylint&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation:&lt;/strong&gt; Tasks are self-documenting in &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;You now have a solid foundation for local Snowflake Python development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Deterministic Python versions&lt;/strong&gt; with &lt;code&gt;pyenv&lt;/code&gt; and &lt;code&gt;.python-version&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Robust dependency management&lt;/strong&gt; with &lt;code&gt;Poetry&lt;/code&gt; and &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Consistent tooling&lt;/strong&gt; configured in a single file&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Simplified task execution&lt;/strong&gt; with &lt;code&gt;Poe the Poet&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup eliminates the "it works on my machine, but not on others" problem at its source. Every developer on your team will have the exact same environment, the same dependencies, and the same tooling automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The DevX payoff:&lt;/strong&gt; By investing in these foundations, you're not just setting up tools, you're creating an environment where Data Engineers can focus on building features instead of fighting with configuration. This is how we democratize data development.&lt;/p&gt;

&lt;p&gt;I hope you find this guide helpful. If you have questions or feedback, I'd love to hear from you!&lt;/p&gt;

</description>
      <category>python</category>
      <category>dataengineering</category>
      <category>snowflake</category>
    </item>
    <item>
      <title>Improving our frontend tracking with Avo</title>
      <dc:creator>Clément Raul</dc:creator>
      <pubDate>Wed, 03 Dec 2025 14:08:23 +0000</pubDate>
      <link>https://dev.to/superpayments/improving-our-frontend-tracking-with-avo-50cc</link>
      <guid>https://dev.to/superpayments/improving-our-frontend-tracking-with-avo-50cc</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;This article explains how we’ve revamped our product analytics frontend tracking at Super using &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt; 📊. For a long time, we relied on Google Sheets to document frontend events, which led to unclear ownership, inconsistent schemas, and slow, manual QA in Segment. We’ve since moved to Avo’s Tracking Plan and Inspector, giving us a single source of truth, a proper branching and peer review process with developers, and automated validation. &lt;br&gt;
➡️ The result: cleaner data, faster debugging, and much smoother collaboration between data and engineering ✅.&lt;/p&gt;

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

&lt;p&gt;Accurate tracking is essential for reliable data monitoring. It helps us confirm that newly released features work as expected, identify and fix bugs, and optimise key user journeys – for example, the funnel for the Super Credit application.&lt;/p&gt;

&lt;p&gt;When tracking goes wrong, the symptoms can vary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing events&lt;/li&gt;
&lt;li&gt;Missing properties&lt;/li&gt;
&lt;li&gt;Typos in property values&lt;/li&gt;
&lt;li&gt;Duplicate events being sent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But the root cause is almost always the same: poor or missing documentation.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Our previous setup: Google Sheets as a tracking plan 📄
&lt;/h2&gt;

&lt;p&gt;Until recently, our main solution for documenting frontend tracking was Google Sheets. For each new feature, we would either create a new document or add a new tab listing all the events that needed to be tracked.&lt;/p&gt;

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

&lt;p&gt;What worked well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It was simple and familiar for everyone.&lt;/li&gt;
&lt;li&gt;The data team could quickly spin up a new sheet and share it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data team was responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating and maintaining the event list&lt;/li&gt;
&lt;li&gt;Sending it to the dev team when new frontend tracking was required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, the limitations quickly became obvious.&lt;/p&gt;

&lt;p&gt;Key pain points ⚠️:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Poor versioning&lt;/strong&gt;: It was difficult to see when events had been removed or updated, and why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unclear ownership&lt;/strong&gt;: Anyone could edit the sheet, and changes often went unnoticed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak review process&lt;/strong&gt;: There was no clear “branching” or peer review flow before sending tracking specs to developers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No automated validation&lt;/strong&gt;: We had no way to systematically check that frontend tracking had been implemented correctly. Validating events in Segment’s debugger was manual, time-consuming, and especially painful for complex features like Super Credit with many different paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Little support for harmonisation&lt;/strong&gt;: There was nothing to enforce reuse of existing properties, ensure consistent property names/values across features, or keep our schema tidy over time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because of these limitations, we decided to look for a better solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exploring alternatives and discovering &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;One option we considered was documenting our tracking events in JSON files and using GitHub for version control, branching, and reviews. This would have been free and would have given us better structure, but it would also have been fairly developer-centric and not very user-friendly for non-engineers.&lt;/p&gt;

&lt;p&gt;After some research, we came across &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt;, a tool focused on frontend tracking schema management, observability, and monitoring.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt; offers two main components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tracking Plan&lt;/li&gt;
&lt;li&gt;Inspector&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Tracking Plan: a single source of truth 📘
&lt;/h2&gt;

&lt;p&gt;The Tracking Plan is where we define all the events sent from the frontend via Segment.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt;, events can be organised by category – for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App events&lt;/li&gt;
&lt;li&gt;Super Credit&lt;/li&gt;
&lt;li&gt;Merchant checkout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each event includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A clear definition of when the event is triggered&lt;/li&gt;
&lt;li&gt;The list of properties to send (e.g. brandId, pageName, memberId)&lt;/li&gt;
&lt;li&gt;The allowed values and formats for those properties, where relevant&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;What &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt; improves:&lt;/p&gt;

&lt;p&gt;✅&lt;strong&gt;Single source of truth&lt;/strong&gt;: All frontend tracking specs live in one structured place instead of being scattered across multiple Google Sheets.&lt;/p&gt;

&lt;p&gt;✅&lt;strong&gt;Branching and reviews&lt;/strong&gt;: Adding or updating events happens via branches, similar to a development workflow. A contributor creates a branch, a peer reviews it, then it’s sent to developers for implementation and finally merged into the main frontend tracking plan once implemented.&lt;/p&gt;

&lt;p&gt;✅&lt;strong&gt;Better versioning&lt;/strong&gt;: It’s easy to see when events are created, changed, or archived.&lt;/p&gt;

&lt;p&gt;✅&lt;strong&gt;Consistency and harmonisation&lt;/strong&gt;: &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt; encourages consistent event naming and property reuse, and helps keep property values aligned across features.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Inspector: validating implementation automatically 🔎
&lt;/h2&gt;

&lt;p&gt;The second major feature we use is the Inspector.&lt;/p&gt;

&lt;p&gt;The Inspector connects Segment to &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt; so that &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt; can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read the events coming from the frontend&lt;/li&gt;
&lt;li&gt;Compare them against the definitions in the Tracking Plan&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This is extremely useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checking that events have been implemented correctly 🤝&lt;/li&gt;
&lt;li&gt;Spotting typos in property names or values&lt;/li&gt;
&lt;li&gt;Ensuring that all required properties are being sent as defined&lt;/li&gt;
&lt;li&gt;What used to require manual QA in Segment’s debugger can now be done much more quickly and systematically.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How we are using &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt; today
&lt;/h2&gt;

&lt;p&gt;We started using &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt; in the context of the Super Credit features. It has already:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improved collaboration between the data team and developers&lt;/li&gt;
&lt;li&gt;Made it easier to review and refine frontend tracking specifications&lt;/li&gt;
&lt;li&gt;Helped us identify and fix tracking bugs more quickly and efficiently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the moment, we’re using the free version of &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt;, which comes with some limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A cap on the volume of events that Inspector can analyse per month (currently 100,000 events)&lt;/li&gt;
&lt;li&gt;Some paid features that we don’t yet have access to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether upgrading to the paid version would be worth it is still under review.&lt;/p&gt;

&lt;p&gt;We are also in the process of migrating our legacy frontend tracking documentation. Around 80% of event definitions related to frontend checkout and Webflow (including Super Credit) have been moved from Google Sheets to &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt;. The next step is to complete the migration for app-related frontend events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion 🎯
&lt;/h2&gt;

&lt;p&gt;Overall, our experience with &lt;a href="https://www.avo.app/" rel="noopener noreferrer"&gt;Avo&lt;/a&gt; has been extremely positive. It is user-friendly, has saved us significant time, and has improved collaboration both within the data team and between data and development.&lt;/p&gt;

&lt;p&gt;By moving away from ad-hoc Google Sheets towards a proper schema management and observability tool, we’ve made our tracking more reliable, our debugging faster, and our analytics more trustworthy – which ultimately helps us build and improve features like Super Credit with much more confidence.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>schema</category>
    </item>
    <item>
      <title>How We Use OpenAI and Gemini Batch APIs to Qualify Thousands of Sales Leads</title>
      <dc:creator>Jag Thind</dc:creator>
      <pubDate>Tue, 09 Sep 2025 11:38:32 +0000</pubDate>
      <link>https://dev.to/superpayments/how-we-use-openai-and-gemini-batch-apis-to-qualify-thousands-of-sales-leads-2knk</link>
      <guid>https://dev.to/superpayments/how-we-use-openai-and-gemini-batch-apis-to-qualify-thousands-of-sales-leads-2knk</guid>
      <description>&lt;p&gt;The following blog details how the Data team used AI to solve a specific problem for our Marketing and Sales teams - &lt;strong&gt;Qualify 3000 websites (Salesforce Accounts) to determine if they are ecommerce and can take payments.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is broken down into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Problem at hand and what are we trying to solve?&lt;/li&gt;
&lt;li&gt;Process design&lt;/li&gt;
&lt;li&gt;Why use LLMs from 2 AI providers&lt;/li&gt;
&lt;li&gt;Prompt engineering and using prompt templates&lt;/li&gt;
&lt;li&gt;Scaling up using OpenAI batch API and google batch predictions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;We implemented a batch data enrichment pipeline that uses OpenAI and Gemini Large Large Models (LLMs) via the &lt;a href="https://platform.openai.com/docs/guides/batch" rel="noopener noreferrer"&gt;OpenAI Batch API&lt;/a&gt; and &lt;a href="https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/batch-prediction-gemini" rel="noopener noreferrer"&gt;Google Batch Predictions&lt;/a&gt; for a cost effective way to enrich data using the power of LLMs.&lt;/p&gt;

&lt;p&gt;To ensure maximum accuracy and minimise the effects of hallucinations from the LLMs, we use a simple consensus system: each website is checked by both AIs &lt;strong&gt;3 times each&lt;/strong&gt;, and only results where they agree are accepted. Yes, this makes it more expensive, but we optimised for &lt;em&gt;time to value&lt;/em&gt; and getting good leads into the hands of the Sales team.&lt;/p&gt;

&lt;p&gt;We used a prompt template configured to use the &lt;a href="https://platform.openai.com/docs/guides/tools-web-search" rel="noopener noreferrer"&gt;web search tool&lt;/a&gt; to ground the LLM with real-time information about the website, overcoming the model's static knowledge cutoff date.&lt;/p&gt;

&lt;p&gt;We trained the Marketing team in writing effective prompts for the LLMs before we scaled up using the batch mode.&lt;/p&gt;

&lt;p&gt;A great example of tech and the business working together to achieve a shared outcome and spreading the use of AI in the business.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem at Hand
&lt;/h2&gt;

&lt;p&gt;The Marketing team periodically builds lists of potential merchants that can integrate Super as a payment method on their website checkout. These leads are then provided to Account Executives (AEs) to sign up.&lt;/p&gt;

&lt;p&gt;When assigned a website the first thing AEs do is manually double check &lt;em&gt;is the website ecommerce&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can you buy products on the website?&lt;/li&gt;
&lt;li&gt;Is there a checkout on the website?&lt;/li&gt;
&lt;li&gt;Does it accept card payments?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚠️ Many websites were not ecommerce ⚠️ resulting in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AEs wasting time doing manual checking&lt;/li&gt;
&lt;li&gt;Many leads getting dis-qualified at the top of the sales funnel&lt;/li&gt;
&lt;li&gt;AEs getting frustrated with leads they were assigned&lt;/li&gt;
&lt;li&gt;AEs resorting to self-sourcing leads and taking them away from their core responsibilities of closing deals&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What are we trying to solve?
&lt;/h3&gt;

&lt;p&gt;Questions we asked ourselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can we increase the number of leads at the top of the sales funnel?&lt;/li&gt;
&lt;li&gt;Can we automate the &lt;em&gt;is ecommerce&lt;/em&gt; check instead of manually qualifying each website?&lt;/li&gt;
&lt;li&gt;Can we scale this check across &lt;em&gt;N&lt;/em&gt; (hundreds/thousands) websites?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Process Design
&lt;/h2&gt;

&lt;p&gt;Before we dive into the details of Prompt Engineering and how the Batch pipeline works. The below illustrates the process and its 2 parts.&lt;/p&gt;

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

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




&lt;h2&gt;
  
  
  Why use LLMs from 2 AI Providers?
&lt;/h2&gt;

&lt;p&gt;Even though it was more costly to do so, we needed to be confident in the accuracy of what we were telling the AEs in the Sales team. Instead of relying on a single AI, we used LLMs from two different AI providers, then based our final decision on their consensus.&lt;/p&gt;

&lt;p&gt;Think of it like getting a second opinion from a trusted expert. If two independent specialists examine the same data and come to the same conclusion, your confidence in that outcome increases dramatically.&lt;/p&gt;

&lt;p&gt;Some benefits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Accuracy Through Consensus&lt;/code&gt;: The core of our strategy is built on consensus. An ecommerce qualification is only confirmed if both LLMs independently agree. This simple but powerful rule acts as a powerful filter, significantly reducing the risk of a single LLM making a mistake, hallucinating, or misinterpreting a site.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Mitigating Model-Specific Weaknesses&lt;/code&gt;: Every LLM has its own unique architecture, training data, and inherent biases. One LLM might be brilliant at identifying traditional retail sites but struggle with subscription services, while the other might have the opposite strengths. Using a single LLM means you also inherit all of its blind spots. By using two, we diversify our "cognitive portfolio," allowing the strengths of one LLM to compensate for the weaknesses of the other, leading to a more balanced and consistently accurate outcome.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Automatic Quality Control&lt;/code&gt;: Perhaps the most valuable benefit is what happens when the LLMs disagree. A disagreement is a critical signal. It tells us that a website is ambiguous, an edge case, or complex in a way that could have easily fooled a single AI. Our system automatically flags these disagreements for manual review.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prompt Engineering
&lt;/h2&gt;

&lt;p&gt;Prompt engineering is the process of writing effective instructions for a LLM, such that it consistently generates content that meets your requirements.&lt;/p&gt;

&lt;p&gt;We used the &lt;a href="https://platform.openai.com/" rel="noopener noreferrer"&gt;OpenAI developer platform&lt;/a&gt; to iteratively develop a &lt;a href="https://platform.openai.com/docs/guides/text?api-mode=responses#reusable-prompts" rel="noopener noreferrer"&gt;reusable prompt&lt;/a&gt; template that could be used in the &lt;a href="https://platform.openai.com/docs/api-reference/responses/create" rel="noopener noreferrer"&gt;responses API&lt;/a&gt;. The platform allows testing different versions of a prompt side-by-side to evaluate changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advantages of using a Prompt Template
&lt;/h3&gt;

&lt;p&gt;You can use variables via &lt;code&gt;{{placeholder}}&lt;/code&gt; and your integration code remains the same, e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;response = client.responses.create(
    model="gpt-4.1",
    prompt={
        "id": "pmpt_abc123",
        "version": "2",
        "variables": {
            "website_url": "xyz.com"
        }
    }
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also configure the prompt to use the &lt;a href="https://platform.openai.com/docs/guides/tools-web-search" rel="noopener noreferrer"&gt;web search tool&lt;/a&gt; to allow the LLM to search the web for the latest information before generating a response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "type": "web_search_preview",
    "user_location": {
        "type": "approximate",
        "country": "GB",
        "search_context_size": "high",
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Prompt Template
&lt;/h3&gt;

&lt;p&gt;The Marketing team produced a prompt template that had clear instructions for the LLM to check if a &lt;em&gt;single&lt;/em&gt; website URL is ecommerce.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please research the website {{url}} provided by the user. You must only return the data requested in the "InformationRequested" section and in a format according to the "OutputFormat" section. Do not include any explanations, reasoning, or commentary.

## InformationRequested
- url: {{url}}
- is_url_valid: Y/N — Is the URL valid and accessible?
- is_ecommerce: Y/N - You MUST use rules from section "Evaluation Rules for column is_ecommerce"

## OutputFormat
Output as JSON with the following fields. Do not include markdown around the JSON:
- url
- is_url_valid
- is_ecommerce

## Evaluation Rules for column is_ecommerce

*Mark "Y" only if all of the following are true, based on explicit evidence available*:
* rule 1
* rule 2
* etc

*Mark "N" in any of the following cases*:
* rule 1
* rule 2
* etc

## Final Reminder

- You must only return the data requested in the "InformationRequested" section.
- You must only return it in the format according to the "OutputFormat" section.
- You must not include any explanations, reasoning, or commentary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Scaling it up - OpenAI Batch API
&lt;/h2&gt;

&lt;p&gt;OpenAI has a &lt;a href="https://platform.openai.com/docs/guides/batch" rel="noopener noreferrer"&gt;Batch API&lt;/a&gt; that allows you to send asynchronous groups of requests with 50% lower costs, a separate pool of significantly higher rate limits, and a clear 24-hour turnaround time. The workflow is:&lt;/p&gt;

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

&lt;p&gt;The uploaded batch file containing the requests will have one line per website as below,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"custom_id": "request-[1756480801.159196]-xyz.com", "method": "POST", "url": "/v1/responses", "body": {"model": "gpt-4.1", "input": "Run the following prompt", "prompt": {"id": "pmpt_XXX", "version": "2", "variables": {"url": "xyz.com"}}}}
{"custom_id": "request-[1756480802.1434196]-abc.com", "method": "POST", "url": "/v1/responses", "body": {"model": "gpt-4.1", "input": "Run the following prompt", "prompt": {"id": "pmpt_XXX", "version": "2", "variables": {"url": "abc.com"}}}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benefits of this are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Significant Cost Reduction&lt;/code&gt;: The 50% discount on pricing is a major advantage for processing thousands of URLs, leading to substantial cost savings compared to using the real-time API.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Increased Throughput&lt;/code&gt;: The much higher rate limits allow for processing a large volume of requests in parallel, drastically reducing the overall time it takes to enrich a large dataset.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Asynchronous "Fire-and-Forget" Workflow&lt;/code&gt;: You can submit a large batch job and not have to wait for it to complete. This is perfect for non-time-sensitive, offline processing tasks, as you can retrieve the results later without keeping a connection open.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Simplified Client-Side Logic&lt;/code&gt;: It removes the need for you to build and maintain complex logic to handle rate limiting, concurrent requests, and retries. You simply prepare and upload a file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Enhanced Resilience and Error Handling&lt;/code&gt;: Since requests are independent, the success or failure of one doesn't impact others. The output file clearly indicates the status of each request, making it easy to identify and retry only the failed ones.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Up to date context&lt;/code&gt;: The prompt template is configured to use the &lt;a href="https://platform.openai.com/docs/guides/tools-web-search" rel="noopener noreferrer"&gt;web search tool&lt;/a&gt; to ground the LLM with real-time information about the website. This search is performed independently for each website.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Scaling it up - Google Batch Predictions
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/batch-prediction-gemini" rel="noopener noreferrer"&gt;Google Batch Predictions&lt;/a&gt; also allows you to generate predictions from Gemini models using a &lt;em&gt;Batch Job&lt;/em&gt;, the workflow is:&lt;/p&gt;

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

&lt;p&gt;Similar to OpenAI the batch job file contains one request per line, but you cannot use a prompt template, so each request in the file has the full prompt. Also web search tools in Gemini are not available via Batch Predictions, but we still found the results to be accurate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where we Ended Up
&lt;/h2&gt;

&lt;p&gt;We now have a repeatable way to enrich data using the power of LLMs for a large number of websites. We have already started using it to conduct other checks.&lt;/p&gt;

&lt;p&gt;The Salesforce Accounts we enriched with &lt;code&gt;is commerce = Y/N&lt;/code&gt; was used to create a better qualified list at the top of the sales funnel.&lt;/p&gt;

&lt;p&gt;AEs were no longer reporting the website as &lt;strong&gt;not&lt;/strong&gt; ecommerce.&lt;/p&gt;

&lt;p&gt;A job well done by AI and Humans!&lt;/p&gt;

</description>
      <category>openai</category>
      <category>gemini</category>
      <category>ai</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>📈 Period-over-Period Measures in Looker: A Simpler, Better Way to Analyze Time Trends</title>
      <dc:creator>Clément Raul</dc:creator>
      <pubDate>Tue, 22 Jul 2025 12:50:48 +0000</pubDate>
      <link>https://dev.to/superpayments/period-over-period-measures-in-looker-a-simpler-better-way-to-analyze-time-trends-3le3</link>
      <guid>https://dev.to/superpayments/period-over-period-measures-in-looker-a-simpler-better-way-to-analyze-time-trends-3le3</guid>
      <description>&lt;p&gt;Tracking change over time—month-over-month, year-over-year—is essential for monitoring performance. Until recently, doing this in Looker meant relying on table calculations, which could be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hard to read and maintain&lt;/li&gt;
&lt;li&gt;Prone to human error&lt;/li&gt;
&lt;li&gt;Limited to visualization-only logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Looker's new period-over-period measures help bring time-based comparisons into LookML itself - making it easier to build reliable, reusable metrics. (&lt;a href="https://cloud.google.com/looker/docs/period-over-period" rel="noopener noreferrer"&gt;https://cloud.google.com/looker/docs/period-over-period&lt;/a&gt;)&lt;/p&gt;




&lt;h2&gt;
  
  
  🧱 Before: Table Calculations
&lt;/h2&gt;

&lt;p&gt;Previously, getting last year’s value for a metric required a &lt;strong&gt;workaround&lt;/strong&gt; like the following table calculation:&lt;br&gt;
&lt;code&gt;offset(${payment_transactions_fact.count_successful_transactions}, 12)&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;Limitations we encountered with this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚠️ Only works in visualizations—can’t be filtered or reused elsewhere&lt;/li&gt;
&lt;li&gt;🧩 Logic cannot get duplicated across dashboards&lt;/li&gt;
&lt;li&gt;🔍 Harder to QA and understand over time&lt;/li&gt;
&lt;li&gt;📉 Fragile if the time grain or sort order changes&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  ✅ Now: LookML Period-over-Period Measures
&lt;/h2&gt;

&lt;p&gt;With the period_over_period measure type, we can now move this logic into our LookML layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Last Year’s Value&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;measure: count_successful_transactions_last_year {
  type: period_over_period
  description: "Successful transactions from the previous year"
  based_on: count_successful_transactions
  based_on_time: created_year
  period: year
  kind: previous
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example: Year-over-Year % Change&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;measure: count_successful_transactions_last_year_relative_change {
  type: period_over_period
  description: "Year-over-year % change in successful transactions"
  based_on: count_successful_transactions
  based_on_time: created_year
  period: year
  kind: relative_change
  value_format_name: percent_0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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




&lt;h2&gt;
  
  
  🏢 How We Use It
&lt;/h2&gt;

&lt;p&gt;At Super, we’ve started using these measures to track key metrics like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📦 Sales – Year-over-year trends&lt;/li&gt;
&lt;li&gt;🧲 Customer and merchant retention – Month-over-month comparisons&lt;/li&gt;
&lt;li&gt;🚀 Feature impact – Week-over-week shifts after a product launch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because these are built into our LookML layer, they can be reused across different dashboards, which helps ensure consistency and saves time.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 Why This Helps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Cleaner and centralized logic – Easier to review and test&lt;/li&gt;
&lt;li&gt;📊 Works across dashboards – Filterable, sortable, and visualization-friendly&lt;/li&gt;
&lt;li&gt;🔧 Adaptable – Supports different time periods (week, month, quarter, year)&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;This feature might seem small, but it's been a noticeable improvement for our data team. It reduces repetitive work, helps standardise time-based comparisons, and makes dashboards easier to maintain and trust.&lt;/p&gt;

</description>
      <category>data</category>
      <category>analytics</category>
      <category>looker</category>
    </item>
    <item>
      <title>Isolating Integration Tests</title>
      <dc:creator>Sam Adams</dc:creator>
      <pubDate>Fri, 04 Jul 2025 05:57:12 +0000</pubDate>
      <link>https://dev.to/superpayments/isolating-integration-tests-2gjd</link>
      <guid>https://dev.to/superpayments/isolating-integration-tests-2gjd</guid>
      <description>&lt;p&gt;When we write integration tests at Super we follow some principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each test should work independently of any other test&lt;/li&gt;
&lt;li&gt;It should only act and assert at the boundary of the service (i.e. how the service is interacted with by other services or users)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In practice this means we have to do some work to manage  isolation (or lack of state-bleed) between each test.&lt;/p&gt;

&lt;p&gt;(Note: if you want to skip to a working code example here is an example repo: &lt;a href="https://github.com/sam-super/example-db-test-isolation" rel="noopener noreferrer"&gt;https://github.com/sam-super/example-db-test-isolation&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;When thinking about isolation it's handy to have a good mental model of how tests are executed in the most popular testing frameworks:&lt;/p&gt;

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

&lt;p&gt;This means our test suites run in parallel, but (by default) each test in the suite run in sequence). So it's important that we use this model to make sure our tests can run in parallel and stay isolated.&lt;/p&gt;

&lt;p&gt;Below is an example of testing a simple fastify app that has a DB with a single table for &lt;code&gt;cars&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;So for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;FastifyInstance&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fastify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;knexFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Knex&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;knex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;execa&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;buildApp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../src/app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;randomString&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./helpers/utils&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startContainers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="s2"&gt;`docker compose up --remove-orphans --wait -d postgres`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initDb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testSpecificDbName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connOpts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// these need to match your docker-compose.yml&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;54323&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whatever&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;connOpts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CREATE DATABASE &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;testSpecificDbName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;knex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;knexFactory&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;connOpts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;testSpecificDbName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;migrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;__dirname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/../src/migrations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;knex_migrations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;knex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;migrate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Created DB &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;testSpecificDbName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (in &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;knex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cars api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FastifyInstance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;knex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Knex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;startContainers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`test_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;randomString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;knex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;initDb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_NAME&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;knex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;knex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;can get a car it creates&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cars&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;make&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ford&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;make&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ford&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cars&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;&lt;span class="na"&gt;make&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ford&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}]);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gets no cars if none created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cars&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What is this doing:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;once, before any tests run, we start our containers (postgres - see docker compose file in &lt;a href="https://github.com/sam-super/example-db-test-isolation" rel="noopener noreferrer"&gt;GH repo&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;before each test we create a new database with a random name and run the knex migrations (which creates the 'cars' table).&lt;/li&gt;
&lt;li&gt;create a new instance of the fastify app (which is bound to the random db name/instance)&lt;/li&gt;
&lt;li&gt;make requests to our api using the inbuilt &lt;code&gt;.inject()&lt;/code&gt; method to issue http requests to fastify (without having to start a http server)&lt;/li&gt;
&lt;li&gt;we can see the second test doesn't have the state (db row) created in the first test (otherwise it would fail)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In future articles we can go into more depth on how to optimize our tests and how we work with isolation when using &lt;code&gt;localstack&lt;/code&gt; (dynamo, sqs queues etc).&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why not just re-use the same database?
&lt;/h3&gt;

&lt;p&gt;We could re-use the same db for each test and truncate the data between each test. We have to be careful tho, because, although in our example we have a single test file/suite, in practice we want our suites to be able to run in parallel. When we have many test-suites, it is advantageous to re-use databases on a per-thread basis (to save the time to create/migrating the DB) and then just truncate the tables between tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isn't this slow?
&lt;/h3&gt;

&lt;p&gt;On an M1 Macbook it's about 10ms to create each DB and run the migrations. It's only 1 migration, and as the number grows so will the time. However, for us it's worth the trade of for what we are trying to achieve and the guaranteed isolation it gives us. &lt;br&gt;
There are also a few strategies to speed it up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;As above we can re-use databases per-worker-thread&lt;/li&gt;
&lt;li&gt;we can maintain a single sql dump file (generated from the migrations themselves) and use it populate the databases on creation (rather than running each individual migration).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Shouldn't you be deleting the DBs and removing the containers at the end?
&lt;/h3&gt;

&lt;p&gt;Re-using the containers speeds up our tests (&lt;code&gt;postgres&lt;/code&gt; is fast to start, but it helps a lot if you have something like &lt;code&gt;localstack&lt;/code&gt; which takes a while to start). Since our tests are isolated, it shouldn't matter what state we leave lying around on our containers. Eventually we could fill up the disk with all our test DBs, but that will take a long time and is easily fixed by killing our containers: &lt;code&gt;docker compose down -v&lt;/code&gt;. Then next test run will bring up clean containers.&lt;/p&gt;

&lt;p&gt;There is also the added benefit of having the DB left around after each test to manually inspect after a failed test run.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>testing</category>
      <category>ci</category>
    </item>
    <item>
      <title>Backend Testing At Super Payments</title>
      <dc:creator>Sam Adams</dc:creator>
      <pubDate>Fri, 04 Jul 2025 05:33:25 +0000</pubDate>
      <link>https://dev.to/superpayments/backend-testing-at-super-payments-5gkd</link>
      <guid>https://dev.to/superpayments/backend-testing-at-super-payments-5gkd</guid>
      <description>&lt;p&gt;At Super Payment we adopt an "integration-first" testing methodology, with unit/e2e style tests being applied more sparingly.&lt;/p&gt;

&lt;p&gt;If you want to read more on why we prefer integration tests to unit/e2e tests then &lt;a href="https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications" rel="noopener noreferrer"&gt;Kent Dodds&lt;/a&gt; has done a much better job than I will &lt;a href="https://kentcdodds.com/blog/write-tests" rel="noopener noreferrer"&gt;on explaining the rationale&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To understand how our integration tests work at Super, we need to know a bit more about our architecture:&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;At Super, our services are written in TypeScript and each service encompasses a given domain. Each service has it's own DB, SQS queue and EventBridge Bus (or whichever subset it needs).&lt;/p&gt;

&lt;p&gt;The main way for our services to talk to each other internally is via EventBridge/SQS messages, where EB receives messages as the output of the producing system, and the consuming system binds the EB messages it's interested in to it's own, dedicated SQS queue for consumption.&lt;/p&gt;

&lt;p&gt;The main way the outside world (customer, third-parties etc) interact with Super is via HTTP (or a partner EventBridge).&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Integration Tests
&lt;/h2&gt;

&lt;p&gt;When we say 'integration test' we mean hitting a single service with some real-world input (an event or HTTP request) to see what outputs/side-effects occur.&lt;/p&gt;

&lt;p&gt;Since most of our services follow the same set of inputs (HTTP/SQS) and outputs (HTTP/EventBridge), our tests try to only act and assert on those inputs and outputs (ignoring 'how' the service works internally).&lt;/p&gt;

&lt;p&gt;A typical integration test at Super looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;start supporting containers (postgres and &lt;a href="https://docs.localstack.cloud/overview/" rel="noopener noreferrer"&gt;localstack&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;start the app (we use &lt;a href="https://nestjs.com/" rel="noopener noreferrer"&gt;NestJS&lt;/a&gt; with &lt;a href="https://fastify.dev/" rel="noopener noreferrer"&gt;fastify&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;spin up a new clean DB for that test (we use &lt;a href="https://knexjs.org/guide/migrations.html#migration-api" rel="noopener noreferrer"&gt;Knex migrations&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;use &lt;a href="https://github.com/nock/nock" rel="noopener noreferrer"&gt;nock&lt;/a&gt; to intercept all network requests&lt;/li&gt;
&lt;li&gt;make HTTP requests to the app and/or send SQS events&lt;/li&gt;
&lt;li&gt;assert on what happened&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We can assert on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the HTTP requests we made to third-party APIs (nock)&lt;/li&gt;
&lt;li&gt;the events we sent (using nock to monitor the EventBridge requests)&lt;/li&gt;
&lt;li&gt;that the SQS queue has been processed/is empty (by directly calling localstack with the AWS-SQS SDK)&lt;/li&gt;
&lt;li&gt;the database state (if we have to)&lt;/li&gt;
&lt;li&gt;the response from our starting HTTP request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since our tests are fully stateful (but fully isolated from other tests) we can see how changes to inputs affect outputs.&lt;/p&gt;

&lt;p&gt;In this series we will dig into the finer details on the above, specifically on how we isolate our tests (while keeping them fast).&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>testing</category>
      <category>ci</category>
    </item>
    <item>
      <title>The Impact of AI on Organizations</title>
      <dc:creator>Aidan McGinley</dc:creator>
      <pubDate>Fri, 20 Jun 2025 12:30:08 +0000</pubDate>
      <link>https://dev.to/superpayments/the-impact-of-ai-on-organizations-2ci4</link>
      <guid>https://dev.to/superpayments/the-impact-of-ai-on-organizations-2ci4</guid>
      <description>&lt;p&gt;What if everything we believe about AI's impact on organizational structure is backward? The widespread assumption is that artificial intelligence will inevitably lead to smaller, leaner teams.  This is characterised by the pithy comment 'Do more with less'.  However that assumption may be fundamentally wrong. &lt;/p&gt;

&lt;p&gt;Here, I outline why the organizations that thrive in the AI era won't be those that downsize, but those that leverage AI to build and manage larger, more impactful teams than were previously possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Economics of Team Size
&lt;/h2&gt;

&lt;p&gt;To understand how AI will impact optimal team size, we first need to understand what determines team size in the first place. In any organization, there are two key functions that determine the optimal number of employees: the cost function and the value function.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Cost Function
&lt;/h3&gt;

&lt;p&gt;Adding an employee incurs two types of costs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fixed costs: These are straightforward - salary, benefits, equipment, office space, etc.&lt;/li&gt;
&lt;li&gt;Variable costs: These are subtler but often more significant. They stem from organizational complexity and are based on principles like Dunbar's number and the exponential growth of communication paths as teams expand.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you plot the marginal cost of hiring employee N, you get a hockey stick curve. Early on, fixed costs dominate, resulting in a relatively flat line. But as the team grows, variable costs take over, causing the curve to bend sharply upward. Anyone who has managed a rapidly growing team will recognize this pattern intuitively - it gets exponentially harder to add people past a certain point.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The Value Function
&lt;/h3&gt;

&lt;p&gt;On the value side, let's consider a simplified model of diminishing returns. While real organizations exhibit much more complex patterns, including potential network effects and complementarities between employees, a basic diminishing returns model can still offer useful insights. In this simplified view, your first hire delivers significant value, and each subsequent hire adds value, though typically at a decreasing rate. This creates a generally downward-sloping line when we plot the marginal value of employee N. While this is a significant simplification of organizational reality, it serves to illustrate our key points about AI's impact.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The Intersection
&lt;/h3&gt;

&lt;p&gt;The optimal team size occurs where these two curves intersect - the point where the marginal cost of adding another employee equals their marginal value contribution. Past this point, each new hire costs more than the value they bring.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Enter AI
&lt;/h2&gt;

&lt;p&gt;Now, here's where it gets interesting. AI doesn't just shift these curves - it fundamentally reshapes them. Let's examine how:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Value Function Changes:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The baseline value per employee increases as AI amplifies individual productivity&lt;/li&gt;
&lt;li&gt;The diminishing returns effect may be moderated as AI tools support productivity at scale&lt;/li&gt;
&lt;li&gt;AI can facilitate more effective employee specialization&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This causes the value curve to move upwards indicating more value per employee&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cost Function Changes:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The fixed cost component might increase slightly due to AI tool costs&lt;/li&gt;
&lt;li&gt;The variable cost curve may become more manageable as AI helps with certain aspects of organizational complexity&lt;/li&gt;
&lt;li&gt;Some communication and coordination costs can be better handled with AI-powered tools&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These attributes lead to lower costs per employee causing the cost curve to move downwards or to the right&lt;/p&gt;

&lt;p&gt;When you plot these new curves, the intersection point tends to move to the right. This suggests that the optimal team size for an AI-enabled organization could be larger than for a traditional one.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;This insight has profound implications for how we think about AI's impact on organizations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Instead of viewing AI as a replacement for human workers, we should see it as a catalyst for organizational scaling&lt;/li&gt;
&lt;li&gt;The focus should be on how AI can help manage complexity and maintain productivity at scale&lt;/li&gt;
&lt;li&gt;Companies that understand this will gain competitive advantages by building larger, more capable teams that deliver more value&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Here we've covered an economic model for describing how AI will affect organisations.  In the next post we will explore how these theoretical insights translate into practice by examining the impact on a software engineering team.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Improve DBT Incremental Performance on Snowflake using Custom Incremental Strategy</title>
      <dc:creator>Jag Thind</dc:creator>
      <pubDate>Thu, 29 May 2025 11:34:29 +0000</pubDate>
      <link>https://dev.to/superpayments/improve-dbt-incremental-performance-on-snowflake-using-custom-incremental-strategy-3ag3</link>
      <guid>https://dev.to/superpayments/improve-dbt-incremental-performance-on-snowflake-using-custom-incremental-strategy-3ag3</guid>
      <description>&lt;p&gt;The following presents how to improve the performance of the DBT built-in &lt;code&gt;delete-insert&lt;/code&gt; &lt;a href="https://docs.getdbt.com/docs/build/incremental-strategy" rel="noopener noreferrer"&gt;incremental strategy&lt;/a&gt; on &lt;strong&gt;snowflake&lt;/strong&gt; so we can control snowflake query costs. It is broken down into:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Defining the problem, with supporting performance statistics&lt;/li&gt;
&lt;li&gt;Desired solution requirements&lt;/li&gt;
&lt;li&gt;Solution implementation, with supporting performance statistics&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We implemented a DBT &lt;a href="https://docs.getdbt.com/docs/build/incremental-strategy#custom-strategies" rel="noopener noreferrer"&gt;custom incremental strategy&lt;/a&gt;, along with &lt;a href="https://docs.getdbt.com/docs/build/incremental-strategy#about-incremental_predicates" rel="noopener noreferrer"&gt;incremental predicates&lt;/a&gt; to improve snowflake query performance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduced MBs scanned by ~99.68%&lt;/li&gt;
&lt;li&gt;Reduced &lt;a href="https://docs.snowflake.com/en/user-guide/tables-clustering-micropartitions#what-are-micro-partitions" rel="noopener noreferrer"&gt;micro-partitions&lt;/a&gt; scanned by ~99.53%&lt;/li&gt;
&lt;li&gt;Reduced query time from &lt;code&gt;19&lt;/code&gt; seconds to &lt;code&gt;1.3&lt;/code&gt; seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Less data is being scanned, so the snowflake warehouse is waiting less time on I/O, so the query completes faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Custom incremental strategies and incremental predicates are more advanced uses of DBT for incremental processing. But I suppose that’s where you have the most fun, so lets get stuck in!&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;When using the DBT built-in &lt;code&gt;delete-insert&lt;/code&gt; &lt;a href="https://docs.getdbt.com/docs/build/incremental-strategy" rel="noopener noreferrer"&gt;incremental strategy&lt;/a&gt; on large volumes of data, you can get inefficient queries on snowflake when the &lt;code&gt;delete&lt;/code&gt; statement is executed. This means queries take longer and &lt;strong&gt;increase warehouse costs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Taking an example target table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With &lt;code&gt;~458 million&lt;/code&gt; rows&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;~26 GB&lt;/code&gt; in size&lt;/li&gt;
&lt;li&gt;Has &lt;code&gt;~2560&lt;/code&gt; micro-partitions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a DBT model that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is running every &lt;code&gt;30 minutes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Typically there are &lt;code&gt;~100K&lt;/code&gt; rows to merge into the target table on every run. As data can arrive out-of-order, a subsequent run will pick these up, but means it can include rows already processed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With DBT model config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  - name: model_name
    config:
      materialized: "incremental"
      incremental_strategy: "delete+insert"
      on_schema_change: "append_new_columns"
      unique_key: ["dw_order_created_skey"] -- varchar(100)
      cluster_by: ["to_date(order_created_at)"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default &lt;code&gt;delete&lt;/code&gt; SQL generated by DBT, before it inserts data in the same transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;delete from target_table as DBT_INTERNAL_DEST
where (dw_order_created_skey) in (
  select distinct dw_order_created_skey
  from source_temp_table as DBT_INTERNAL_SOURCE
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Performance Statistics
&lt;/h3&gt;

&lt;p&gt;To find the rows in the target table to delete with the matching &lt;code&gt;dw_order_created_skey&lt;/code&gt; (see node profile overview image below), snowflake has to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scan &lt;code&gt;~11 GB&lt;/code&gt; of the target table&lt;/li&gt;
&lt;li&gt;Scan all &lt;code&gt;~2560 micro-partitions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Query takes &lt;code&gt;~19 seconds&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; - The query is &lt;strong&gt;not&lt;/strong&gt; filtering on &lt;code&gt;order_created_at&lt;/code&gt; to allow snowflake to use the &lt;code&gt;clustering key&lt;/code&gt; of &lt;code&gt;to_date(order_created_at)&lt;/code&gt; to find the matching rows to delete. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query plan&lt;/strong&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx4xw5sqxxzu9mmj7mq11.png" alt="node profile overview"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Desired Solution
&lt;/h2&gt;

&lt;p&gt;To limit the data read in the target table above. We can make use of &lt;a href="https://docs.getdbt.com/docs/build/incremental-strategy#about-incremental_predicates" rel="noopener noreferrer"&gt;incremental_predicates&lt;/a&gt; in the model config. This will add SQL to filter the target table.&lt;/p&gt;

&lt;p&gt;DBT model config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  - name: model_name
    config:
      materialized: "incremental"
      incremental_strategy: "delete+insert"
      on_schema_change: "append_new_columns"
      unique_key: ["dw_order_created_skey"]
      cluster_by: ["to_date(order_created_at)"]
      incremental_predicates:
        - "order_created_at &amp;gt;= (select dateadd(hour, -24, min(order_created_at)) from DBT_INTERNAL_SOURCE)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Issues with this&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://docs.getdbt.com/docs/build/incremental-strategy#about-incremental_predicates" rel="noopener noreferrer"&gt;incremental_predicates&lt;/a&gt; docs states &lt;em&gt;dbt does not check the syntax of the SQL statements&lt;/em&gt;, so it does not change anything in the SQL.&lt;/li&gt;
&lt;li&gt;We get an error when it executes on snowflake: &lt;code&gt;Object 'DBT_INTERNAL_SOURCE' does not exist or not authorized.&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;We cannot hardcode the snowflake table name in the incremental_predicates, as its dynamically generated by DBT.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Solution Implementation
&lt;/h2&gt;

&lt;p&gt;We need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do some pre-processing on each element of &lt;code&gt;incremental_predicates&lt;/code&gt; to replace &lt;code&gt;DBT_INTERNAL_SOURCE&lt;/code&gt; with actual &lt;code&gt;source_temp_table&lt;/code&gt; so SQL like the below is generated by DBT for better performance:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;delete from target_table as DBT_INTERNAL_DEST
where (dw_order_created_skey) in (
  select distinct dw_order_created_skey
  from source_temp_table as DBT_INTERNAL_SOURCE
)
-- Added by incremental_predicates
and order_created_at &amp;gt;= (select dateadd(hour, -24, min(order_created_at)) from source_temp_table)
;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Continue to call the &lt;strong&gt;default&lt;/strong&gt; DBT &lt;code&gt;delete+insert&lt;/code&gt; incremental strategy with the new value for &lt;code&gt;incremental_predicates&lt;/code&gt; in the arguments dictionary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How&lt;/strong&gt; - The below macro implements a &lt;em&gt;light-weight&lt;/em&gt; &lt;a href="https://docs.getdbt.com/docs/build/incremental-strategy#custom-strategies" rel="noopener noreferrer"&gt;custom incremental strategy&lt;/a&gt; do this. You can see at the end it calls the default &lt;code&gt;get_incremental_delete_insert_sql&lt;/code&gt; DBT code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{% macro get_incremental_custom_delete_insert_sql(arg_dict) %}
  {% set custom_arg_dict = arg_dict.copy() %}
  {% set source = custom_arg_dict.get('temp_relation') | string %}
  {% set target = custom_arg_dict.get('target_relation') | string %}

  {% if source is none %}
    {{ exceptions.raise_compiler_error('temp_relation is not present in arguments!') }}
  {% endif %}

  {% if target is none %}
    {{ exceptions.raise_compiler_error('target_relation is not present in arguments!') }}
  {% endif %}

  {% set raw_predicates = custom_arg_dict.get('incremental_predicates', []) %}

  {% if raw_predicates is string %}
    {% set predicates = [raw_predicates] %}
  {% else %}
    {% set predicates = raw_predicates %}
  {% endif %}

  {% if predicates %}
    {% set replaced_predicates = [] %}
    {% for predicate in predicates %}
      {% set replaced = predicate
        | replace('DBT_INTERNAL_SOURCE', source)
        | replace('DBT_INTERNAL_DEST', target)
      %}
      {% do replaced_predicates.append(replaced) %}
    {% endfor %}
    {% do custom_arg_dict.update({'incremental_predicates': replaced_predicates}) %}
  {% endif %}

  {{ log('Calling get_incremental_delete_insert_sql with args: ' ~ custom_arg_dict, info=False) }}
  {{ get_incremental_delete_insert_sql(custom_arg_dict) }}
{% endmacro %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is now callable from the DBT model config by setting &lt;code&gt;incremental_strategy&lt;/code&gt; to &lt;code&gt;custom_delete_insert&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  - name: model_name
    config:
      materialized: "incremental"
      incremental_strategy: "custom_delete_insert"
      on_schema_change: "append_new_columns"
      unique_key: ["dw_order_created_skey"]
      cluster_by: ["to_date(order_created_at)"]
      incremental_predicates:
        - "order_created_at &amp;gt;= (select dateadd(hour, -24, min(order_created_at)) from DBT_INTERNAL_SOURCE)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Performance Improvement Statistics
&lt;/h3&gt;

&lt;p&gt;To find &lt;code&gt;~100K&lt;/code&gt; rows to delete in the target table, now snowflake has to only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scan &lt;code&gt;~35 MB&lt;/code&gt; of the target table, 11 GB → 35 MB = &lt;strong&gt;~99.68%&lt;/strong&gt; improvement&lt;/li&gt;
&lt;li&gt;Scan &lt;code&gt;12 micro-partitions&lt;/code&gt;, 2560 → 12 = &lt;strong&gt;~99.53%&lt;/strong&gt; improvement&lt;/li&gt;
&lt;li&gt;Query takes &lt;code&gt;~1.3 seconds&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Less data is being scanned, so the snowflake warehouse is waiting less time on I/O, so the query completes faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query plan&lt;/strong&gt;&lt;/p&gt;

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

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




&lt;p&gt;If you're interested in hearing more about how we use DBT at &lt;a href="https://www.superpayments.com" rel="noopener noreferrer"&gt;Super Payments&lt;/a&gt;, feel free to reach out!&lt;/p&gt;

</description>
      <category>dbt</category>
      <category>snowflake</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Hashicorp Vault at Super</title>
      <dc:creator>Luke Livingstone</dc:creator>
      <pubDate>Thu, 22 May 2025 14:41:55 +0000</pubDate>
      <link>https://dev.to/superpayments/hashicorp-vault-at-super-4lck</link>
      <guid>https://dev.to/superpayments/hashicorp-vault-at-super-4lck</guid>
      <description>&lt;p&gt;At Super, we use HashiCorp Vault to securely store the secrets required by our microservices running on Kubernetes.&lt;/p&gt;

&lt;p&gt;We’ve been long-time fans of Vault. Our Platform team has previous experience deploying and maintaining it, so choosing Vault for our current setup was an easy decision from a knowledge and reliability standpoint.&lt;/p&gt;

&lt;p&gt;Drawing on lessons from past implementations, we were able to build something robust and scalable. Our infrastructure is hosted entirely on AWS and is segmented across multiple accounts. We maintain three separate workload accounts, Staging, Mock, and Production each running Super's microservices in Kubernetes along side a Infrastructure account, for Platform tooling.&lt;/p&gt;

&lt;p&gt;Rather than deploying and maintaining a separate Vault cluster for each environment, we opted for a centralised approach. This decision reduced operational overhead and significantly improved the developer experience, avoiding the complexity of managing and switching between multiple Vault interfaces.&lt;/p&gt;




&lt;p&gt;To get started, we deployed our Vault infrastructure via Terraform. Vault’s storage backend is powered by Amazon S3, with DynamoDB providing high availability. We also use AWS KMS for auto-unseal functionality, eliminating the need for manual intervention when restarting Vault. Vault itself is installed using the &lt;a href="https://github.com/hashicorp/vault-helm" rel="noopener noreferrer"&gt;official HashiCorp Helm chart&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Next, we provisioned an internal Network Load Balancer (NLB) and exposed it through a VPC Endpoint Service. This design choice enables secure, cross-account connectivity to Vault using VPC Interface Endpoints—avoiding the complexity and security risks of VPC peering.&lt;/p&gt;

&lt;p&gt;To simplify service discovery within our Kubernetes clusters, we created human-readable internal services that resolve &lt;code&gt;super.vault&lt;/code&gt; to the appropriate VPC interface endpoint. This gives our services a clean and consistent way to talk to Vault, regardless of the environment they’re running in.&lt;/p&gt;




&lt;p&gt;That wraps up our simple yet effective centralized Vault infrastructure here at Super. By consolidating our setup, we've kept operations streamlined, secure, and developer-friendly across all environments.&lt;/p&gt;

&lt;p&gt;If you're interested in hearing more or want us to dive deeper into any aspect of our Vault implementation—be it authentication flows, secret injection, or scaling—feel free to reach out. We'd love to share more!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>vault</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Karpenter on EKS Fargate</title>
      <dc:creator>Luke Livingstone</dc:creator>
      <pubDate>Wed, 17 Apr 2024 15:04:30 +0000</pubDate>
      <link>https://dev.to/superpayments/using-fargate-on-eks-for-karpenter-37mk</link>
      <guid>https://dev.to/superpayments/using-fargate-on-eks-for-karpenter-37mk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;We're reinventing payments. Super powers free payments for businesses and more rewarding shopping for customers, so that everyone wins. &lt;a href="https://www.superpayments.com/" rel="noopener noreferrer"&gt;https://www.superpayments.com/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;We're using &lt;a href="https://karpenter.sh/" rel="noopener noreferrer"&gt;Karpenter&lt;/a&gt; to manage our Kubernetes node scaling. &lt;/p&gt;

&lt;p&gt;We're big fans of how fast Karpenter can provision just-in-time nodes for us across our EKS clusters but there was one sticking point, for obvious reasons the Karpenter controller pods can't run on Karpenter managed nodes.&lt;/p&gt;

&lt;p&gt;To get around this we used AWS EKS managed node groups as &lt;code&gt;init&lt;/code&gt; nodes and pinned Karpenter to said nodes. We provisioned a node group with a minimum and maximum of 2 nodes for Karpenter mostly (although other pods could run on these nodes too, to avoid wasting compute resources!)&lt;/p&gt;

&lt;p&gt;The downside is that updating managed node groups is slow; updating two nodes, with a maximum of one available at a time, took between six and ten minutes, and we wanted to speed up this process.&lt;/p&gt;

&lt;p&gt;The simple solution? Remove the init nodes! But then, where do we run Karpenter? Enter Fargate.&lt;/p&gt;

&lt;p&gt;We created an EKS Fargate profile via our EKS Terraform module with a selector for the Karpenter namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_eks_fargate_profile" "karpenter" {
  cluster_name           = aws_eks_cluster.cluster.name
  fargate_profile_name   = "karpenter"
  pod_execution_role_arn = aws_iam_role.fargate.arn
  subnet_ids             = var.private_subnets

  selector {
    namespace = "karpenter"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pod Execution Role
&lt;/h3&gt;

&lt;p&gt;If you've ever used ECS, you'll be familiar with the pod execution role. For Fargate and EKS, it's a straightforward role with two AWS managed policies attached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_iam_role" "fargate" {
  name = "${var.cluster_name}-fargate"

  assume_role_policy = jsonencode({
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "eks-fargate-pods.amazonaws.com"
      }
    }]
    Version = "2012-10-17"
  })
}

resource "aws_iam_role_policy_attachment" "fargate" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy"
  role       = aws_iam_role.fargate.name
}

resource "aws_iam_role_policy_attachment" "fargate_eks_cni" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
  role       = aws_iam_role.fargate.name
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Karpenter Helm Install
&lt;/h3&gt;

&lt;p&gt;We use the hashicorp/helm Terraform provider to install both the Karpenter and CRD charts directly from our EKS module. This ensures that Karpenter is up and running before anything else, ready to provision compute.&lt;/p&gt;

&lt;p&gt;Next, we set the namespace for the Karpenter chart to match the selector in the Fargate profile, which in our case is karpenter, and we're off to the races!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                         READY   STATUS    RESTARTS   AGE   IP              NODE                                                  NOMINATED NODE 
karpenter-75c664b7cb-9z9lr   1/1     Running   0          5d    &amp;lt;snip&amp;gt;   fargate-&amp;lt;snip&amp;gt;.eu-west-2.compute.internal   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
karpenter-75c664b7cb-fxhb2   1/1     Running   0          5d    &amp;lt;snip&amp;gt;    fargate-&amp;lt;snip&amp;gt;.eu-west-2.compute.internal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Notes
&lt;/h3&gt;

&lt;p&gt;By default we're using Fargate's minimum resources which are &lt;code&gt;0.25 vCPU&lt;/code&gt; and &lt;code&gt;0.5GB RAM&lt;/code&gt; per task.&lt;/p&gt;

&lt;p&gt;Currently you &lt;a href="https://github.com/aws/containers-roadmap/issues/1629" rel="noopener noreferrer"&gt;can't specify ARM&lt;/a&gt; when creating Fargate tasks on EKS so we're currently using x86 but the cost is around $20 per month for both tasks.&lt;/p&gt;

&lt;p&gt;We've generally reduced the number of nodes across our EKS clusters too, resulting in some cost savings but much less waiting around for the Platform team!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>karpenter</category>
      <category>autoscaling</category>
      <category>devops</category>
    </item>
    <item>
      <title>Terraform Modules at Super</title>
      <dc:creator>Luke Livingstone</dc:creator>
      <pubDate>Thu, 28 Mar 2024 12:30:55 +0000</pubDate>
      <link>https://dev.to/superpayments/terraform-modules-at-super-48kp</link>
      <guid>https://dev.to/superpayments/terraform-modules-at-super-48kp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;We're reinventing payments. Super powers free payments for businesses and more rewarding shopping for customers, so that everyone wins. &lt;a href="https://www.superpayments.com/" rel="noopener noreferrer"&gt;https://www.superpayments.com/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Like most startups, we use Terraform to manage and deploy our infrastructure. This post covers how we use Terraform modules at Super to adhere to the DRY principle.&lt;/p&gt;

&lt;p&gt;Early in our Terraform refactor, we aimed to invest in modules. Our goal was to promote high reusability while minimising code.&lt;/p&gt;

&lt;p&gt;At the time of writing, Super has around 70 Terraform modules in use across 10 providers. Some of the modules are small (e.g. IAM Role) and some are larger (e.g. EKS Cluster).&lt;/p&gt;




&lt;h3&gt;
  
  
  Template Module &amp;amp; Code Style 📝
&lt;/h3&gt;

&lt;p&gt;In order to keep module creation in line with a style guide we have a template module. Some of the rules below are best practice and some are specific to Super.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We don't include provider configurations&lt;/li&gt;
&lt;li&gt;We don't include any backend configuration &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data.tf&lt;/code&gt; file is used for all &lt;code&gt;data&lt;/code&gt; resources&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;outputs.tf&lt;/code&gt; file is used for all output resources&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;variables.tf&lt;/code&gt; file is used for all variables&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;versions.tf&lt;/code&gt; file is used for &lt;code&gt;required_providers&lt;/code&gt; and &lt;code&gt;required_version&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why no provider?! 😱
&lt;/h3&gt;

&lt;p&gt;The primary reason we avoid including a provider in our modules is to facilitate nesting modules. Nesting modules can be beneficial to keep resources used in a standardised format across modules.&lt;/p&gt;

&lt;p&gt;When using a module inside of a module Terraform deems it incompatible with &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;for_each&lt;/code&gt;, and &lt;code&gt;depends_on&lt;/code&gt; if the module in question has its own local provider configuration.&lt;/p&gt;

&lt;p&gt;We started out only removing the providers of modules nested, but decided that we can make use of Terragrunt's &lt;a href="https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#generate" rel="noopener noreferrer"&gt;generate&lt;/a&gt; and &lt;a href="https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#include" rel="noopener noreferrer"&gt;include&lt;/a&gt; to remove providers from all modules.&lt;/p&gt;

&lt;p&gt;Let's take the following directory structure for AWS as an example. We have a folder for the AWS region (eu-west-2) and we also have a few hcl files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;├── super-staging
│   ├── eu-west-2
│   ├── aws.hcl
│   ├── terragrunt.hcl
│   └── vault.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;aws.hcl&lt;/code&gt; file uses a Terragrunt generate block to arbitrarily generate a file in the terragrunt working directory (where terraform is called).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws.tf"&lt;/span&gt;
  &lt;span class="nx"&gt;if_exists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"overwrite_terragrunt"&lt;/span&gt;
  &lt;span class="nx"&gt;contents&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
provider "aws" {
  region = "eu-west-2"
  default_tags {
    tags = {
      environment = "staging",
    }
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using a module with Terragrunt you can then use the include block with the &lt;code&gt;find_in_parent_folders&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"aws.hcl"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"git@github.com:organisation/terraform-example-module.git?ref=v1.0.0"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Remote State
&lt;/h3&gt;

&lt;p&gt;We use S3 as our state store along with DynamoDB for locking all encrypted with KMS.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;terragrunt.hcl&lt;/code&gt; at the root of the directory includes three things. The terragrunt &lt;code&gt;remote_state&lt;/code&gt; block, &lt;code&gt;iam_role&lt;/code&gt; and some default &lt;code&gt;inputs&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;remote_state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt;

  &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backend.tf"&lt;/span&gt;
    &lt;span class="nx"&gt;if_exists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"overwrite"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"super-staging-eu-west-2-example-bucket"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path_relative_to_include()}/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-2"&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;dynamodb_table&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"super-staging-eu-west-2-example-table"&lt;/span&gt;
    &lt;span class="nx"&gt;kms_key_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"alias/s3-super-staging-eu-west-2-example-kms"&lt;/span&gt;
    &lt;span class="nx"&gt;disable_bucket_update&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;iam_role&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::&amp;lt;snip&amp;gt;:role/example-role"&lt;/span&gt;

&lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"staging"&lt;/span&gt;
  &lt;span class="nx"&gt;aws_account_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;snip&amp;gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;service_owner&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"devops"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then add the include like we do with the AWS provider. By default &lt;code&gt;find_in_parent_folders&lt;/code&gt; will search for the first &lt;code&gt;terragrunt.hcl&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"root"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;expose&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Versioning 🔢
&lt;/h3&gt;

&lt;p&gt;Our Platform team are enthusiasts of semantic versioning and we also use conventional commits.&lt;/p&gt;

&lt;p&gt;We have a simple Github Action job on each module repository that uses the &lt;code&gt;semantic-release-action&lt;/code&gt;. We use the &lt;code&gt;@semantic-release/commit-analyzer&lt;/code&gt; plugin with the &lt;a href="https://www.conventionalcommits.org/en/v1.0.0/" rel="noopener noreferrer"&gt;&lt;code&gt;conventionalcommits&lt;/code&gt;&lt;/a&gt; preset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Release&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cycjimmy/semantic-release-action@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;semantic_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;23.0.2&lt;/span&gt;
          &lt;span class="na"&gt;extra_plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;@semantic-release/changelog@6.0.3&lt;/span&gt;
            &lt;span class="s"&gt;@semantic-release/git@10.0.1&lt;/span&gt;
            &lt;span class="s"&gt;conventional-changelog-conventionalcommits@7.0.2&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CI_GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>terraform</category>
      <category>iac</category>
      <category>terragrunt</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
