<?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: Freddie Thompson</title>
    <description>The latest articles on DEV Community by Freddie Thompson (@freddie_thompson_e0488262).</description>
    <link>https://dev.to/freddie_thompson_e0488262</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4163135%2Fdc43b800-dbc8-41d5-bf19-6f69c5742da6.png</url>
      <title>DEV Community: Freddie Thompson</title>
      <link>https://dev.to/freddie_thompson_e0488262</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/freddie_thompson_e0488262"/>
    <language>en</language>
    <item>
      <title>Building AI-Assisted Tax Document Workflows: Engineering Considerations for Tokenization and Evidence</title>
      <dc:creator>Freddie Thompson</dc:creator>
      <pubDate>Tue, 27 Oct 2026 12:45:58 +0000</pubDate>
      <link>https://dev.to/freddie_thompson_e0488262/building-ai-assisted-tax-document-workflows-engineering-considerations-for-tokenization-and-4g67</link>
      <guid>https://dev.to/freddie_thompson_e0488262/building-ai-assisted-tax-document-workflows-engineering-considerations-for-tokenization-and-4g67</guid>
      <description>&lt;p&gt;AI and tokenization are often discussed as separate technology trends. Tax-related workflows offer an opportunity to examine how they might fit into a broader information-management system.&lt;/p&gt;

&lt;p&gt;The important engineering question is not simply how to extract data from a form or create a digital token. It is how to maintain traceability, protect sensitive information, and ensure that downstream decisions can be reviewed.&lt;/p&gt;

&lt;p&gt;SoluLab's AI-powered tax form tokenization and compliance evidence platform case study is a useful project-specific reference for this topic.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Model the Workflow Before Selecting the Stack&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start by documenting the stages that a tax document passes through.&lt;/p&gt;

&lt;p&gt;Depending on the use case, the requirements may include ingestion, classification, extraction, validation, review, evidence storage, and retrieval.&lt;/p&gt;

&lt;p&gt;Each stage should have a defined input, output, failure state, and access policy.&lt;/p&gt;

&lt;p&gt;For example, an extraction failure should not silently produce an accepted compliance record. It should create an exception that can be investigated.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make AI Outputs Verifiable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AI-assisted extraction can reduce repetitive work, but extracted values must remain connected to their sources.&lt;/p&gt;

&lt;p&gt;A robust design should consider:&lt;/p&gt;

&lt;p&gt;Field-level validation.&lt;/p&gt;

&lt;p&gt;Confidence thresholds and human-review queues.&lt;/p&gt;

&lt;p&gt;Handling of missing or conflicting values.&lt;/p&gt;

&lt;p&gt;Versioning of documents and corrections.&lt;/p&gt;

&lt;p&gt;Logging of important processing and approval events.&lt;/p&gt;

&lt;p&gt;The exact controls will depend on the data, risk level, and applicable compliance requirements.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the Tokenization Boundary&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before implementing tokenization, answer a basic question: What exactly does the token represent?&lt;/p&gt;

&lt;p&gt;It might represent a defined asset or another explicitly modeled entity. The architecture must specify the relationship between that representation and its authoritative data.&lt;/p&gt;

&lt;p&gt;Avoid assuming that sensitive tax documents should be stored directly on-chain. Data placement, access requirements, privacy obligations, and the need for corrections must all be assessed first.&lt;/p&gt;

&lt;p&gt;Likewise, do not select a blockchain or token standard until the functional requirements justify that choice.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Design Evidence Retrieval as a Core Feature&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Evidence management should not be an afterthought.&lt;/p&gt;

&lt;p&gt;Developers need to consider provenance, permissions, integrity checks, retention, and how an authorized reviewer can retrieve the supporting source for a particular record.&lt;/p&gt;

&lt;p&gt;A system may demonstrate that a record has not changed without proving that the information was originally correct. That distinction matters in compliance-related applications.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Test Failure Paths, Not Just the Happy Path&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Useful test scenarios include:&lt;/p&gt;

&lt;p&gt;Documents with missing fields.&lt;/p&gt;

&lt;p&gt;Conflicting values across records.&lt;/p&gt;

&lt;p&gt;Low-confidence extraction results.&lt;/p&gt;

&lt;p&gt;Unauthorized evidence requests.&lt;/p&gt;

&lt;p&gt;Corrected or superseded documents.&lt;/p&gt;

&lt;p&gt;Failed processing and retry behavior.&lt;/p&gt;

&lt;p&gt;Testing should evaluate the complete workflow rather than only the performance of the AI component.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;AI processing, tokenization, and evidence management solve different problems. A dependable implementation must define the boundaries between them and make those boundaries testable.&lt;/p&gt;

&lt;p&gt;For developers, the strongest starting point is a clear data model, explicit validation rules, and a documented evidence lifecycle. Technology choices should follow from those requirements—not the other way around.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Blockchain in Banking: What Developers Should Evaluate Before Building</title>
      <dc:creator>Freddie Thompson</dc:creator>
      <pubDate>Mon, 26 Oct 2026 11:45:15 +0000</pubDate>
      <link>https://dev.to/freddie_thompson_e0488262/blockchain-in-banking-what-developers-should-evaluate-before-building-hnl</link>
      <guid>https://dev.to/freddie_thompson_e0488262/blockchain-in-banking-what-developers-should-evaluate-before-building-hnl</guid>
      <description>&lt;p&gt;Blockchain-based banking applications involve more than writing smart contracts or storing transactions on a ledger. Developers must understand the business workflow, system boundaries, security requirements, and integration constraints before choosing an implementation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the Use Case&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start by documenting the existing workflow.&lt;/p&gt;

&lt;p&gt;Which participants need access to the records?&lt;/p&gt;

&lt;p&gt;What information must be shared?&lt;/p&gt;

&lt;p&gt;Who is authorized to submit or verify transactions?&lt;/p&gt;

&lt;p&gt;What are the expected transaction volumes?&lt;/p&gt;

&lt;p&gt;Which existing systems must remain operational?&lt;/p&gt;

&lt;p&gt;These questions help determine whether a blockchain architecture is appropriate.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Design for Security&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Financial applications require careful handling of identities, permissions, sensitive data, and transaction authorization.&lt;/p&gt;

&lt;p&gt;Developers should consider which information belongs on-chain and which should remain in protected off-chain systems. The answer depends on the application and its privacy requirements.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Plan for Integration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Blockchain applications frequently need to exchange information with existing software. API design, data consistency, error handling, monitoring, and recovery procedures should be part of the architecture discussion.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate Performance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before deployment, test transaction throughput, latency, failure scenarios, and expected infrastructure requirements. Performance assumptions should be validated under realistic workloads.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Review a Banking Case Study&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SoluLab's Chichin Blockchain in Banking project is a relevant starting point for exploring a blockchain application in the banking domain.&lt;/p&gt;

&lt;p&gt;Consult the original project details before drawing conclusions about the selected framework, architecture, consensus mechanism, or implementation results.&lt;/p&gt;

&lt;p&gt;Final Thoughts&lt;/p&gt;

&lt;p&gt;The engineering challenge in banking is not simply to introduce blockchain. It is to design a secure, maintainable solution that addresses a clearly identified business requirement.&lt;/p&gt;

&lt;p&gt;Start with the workflow, document the constraints, evaluate the architecture, and test the assumptions before scaling the solution.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building a Multi-Chain DEX: What Blockchain Developers Should Consider</title>
      <dc:creator>Freddie Thompson</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:36:41 +0000</pubDate>
      <link>https://dev.to/freddie_thompson_e0488262/building-a-multi-chain-dex-what-blockchain-developers-should-consider-4ae7</link>
      <guid>https://dev.to/freddie_thompson_e0488262/building-a-multi-chain-dex-what-blockchain-developers-should-consider-4ae7</guid>
      <description>&lt;p&gt;Multi-chain development introduces a practical engineering challenge: how do you support different blockchain environments without making the application unnecessarily complex?&lt;/p&gt;

&lt;p&gt;For decentralized exchanges (DEXs), this question affects blockchain integrations, transaction handling, smart contract security, and the user interface.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with Network Requirements&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before writing integration code, define which blockchain networks the product needs to support and why.&lt;/p&gt;

&lt;p&gt;For each network, document:&lt;/p&gt;

&lt;p&gt;Transaction and confirmation behavior&lt;/p&gt;

&lt;p&gt;Smart contract compatibility&lt;/p&gt;

&lt;p&gt;Available tooling and infrastructure&lt;/p&gt;

&lt;p&gt;Asset identification requirements&lt;/p&gt;

&lt;p&gt;Network-specific failure scenarios&lt;/p&gt;

&lt;p&gt;Avoid adding chains without a clear product or technical reason. Every additional integration increases the testing and maintenance surface.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Separate Network Logic from Application Logic&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A maintainable multi-chain application benefits from clear boundaries between the user interface, business logic, and network-specific integrations.&lt;/p&gt;

&lt;p&gt;A conceptual architecture might look like this:&lt;/p&gt;

&lt;p&gt;User Interface&lt;br&gt;
      |&lt;br&gt;
Application Services&lt;br&gt;
      |&lt;br&gt;
Network Integration Layer&lt;br&gt;
      |&lt;br&gt;
Supported Blockchain Networks&lt;/p&gt;

&lt;p&gt;This is a general architectural illustration, not a claim about the implementation used in any specific project.&lt;/p&gt;

&lt;p&gt;The purpose is to make responsibilities easier to understand and network-specific changes easier to manage.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Design for Transaction Failures&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A transaction may be pending, rejected, reverted, or delayed. The interface should communicate these states clearly and avoid treating transaction submission as proof of successful execution.&lt;/p&gt;

&lt;p&gt;Developers should test:&lt;/p&gt;

&lt;p&gt;Wallet rejection&lt;/p&gt;

&lt;p&gt;RPC or provider failures&lt;/p&gt;

&lt;p&gt;Insufficient funds&lt;/p&gt;

&lt;p&gt;Reverted transactions&lt;/p&gt;

&lt;p&gt;Delayed confirmations&lt;/p&gt;

&lt;p&gt;Network switching errors&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Include Security in the Development Lifecycle&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Smart contracts, external dependencies, and transaction flows need appropriate security reviews. If the architecture uses cross-chain messaging or bridging, those components require additional threat analysis.&lt;/p&gt;

&lt;p&gt;Do not assume that connecting multiple networks automatically creates cross-chain asset interoperability. That depends on the specific implementation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Plan for Ongoing Maintenance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Network integrations change over time. Monitoring, regression tests, dependency updates, and documented recovery procedures help keep the application maintainable.&lt;/p&gt;

&lt;p&gt;For a project reference, see SoluLab's Multi-Chain DEX case study.&lt;/p&gt;

&lt;p&gt;The project can be explored alongside these general engineering considerations, while implementation-specific details should be taken from the published case study.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;The difficult part of multi-chain DEX development is managing differences between networks without compromising security or usability.&lt;/p&gt;

&lt;p&gt;Start with clear network requirements, define integration boundaries, test failure scenarios, and treat every supported chain as an ongoing engineering responsibility.&lt;/p&gt;

&lt;p&gt;What has been your biggest challenge with multi-chain application development: RPC reliability, transaction handling, or smart contract security?&lt;/p&gt;

&lt;h1&gt;
  
  
  blockchain #web3 #defi #programming
&lt;/h1&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>smartcontract</category>
      <category>web3</category>
    </item>
    <item>
      <title>Building Trust into Digital Document Workflows with Blockchain</title>
      <dc:creator>Freddie Thompson</dc:creator>
      <pubDate>Thu, 22 Oct 2026 11:24:04 +0000</pubDate>
      <link>https://dev.to/freddie_thompson_e0488262/building-trust-into-digital-document-workflows-with-blockchain-2kmk</link>
      <guid>https://dev.to/freddie_thompson_e0488262/building-trust-into-digital-document-workflows-with-blockchain-2kmk</guid>
      <description>&lt;p&gt;The problem developers should start with&lt;/p&gt;

&lt;p&gt;Document management sounds straightforward: upload a file, store it, and retrieve it when needed.&lt;/p&gt;

&lt;p&gt;Real-world requirements can be more complicated. Depending on the application, developers may also need to support document verification, activity traceability, permissions, and reliable records of important actions.&lt;/p&gt;

&lt;p&gt;Blockchain can be considered when these requirements involve a need for tamper-resistant records.&lt;/p&gt;

&lt;p&gt;What should the architecture consider?&lt;/p&gt;

&lt;p&gt;Before writing smart contracts or selecting a blockchain network, define the system's responsibilities.&lt;/p&gt;

&lt;p&gt;Document storage: Determine where files will live and how they will be retrieved.&lt;/p&gt;

&lt;p&gt;Verification: Establish what users need to verify and what evidence the system should maintain.&lt;/p&gt;

&lt;p&gt;Identity: Define how users and organizations are authenticated.&lt;/p&gt;

&lt;p&gt;Authorization: Separate authentication from permissions. Knowing who a user is does not automatically mean they should be able to access every document.&lt;/p&gt;

&lt;p&gt;Privacy: Avoid assuming that putting information on a blockchain makes it private.&lt;/p&gt;

&lt;p&gt;These decisions should be based on the actual application requirements.&lt;/p&gt;

&lt;p&gt;A useful distinction: the file versus its record&lt;/p&gt;

&lt;p&gt;A blockchain-based design may maintain selected records related to a document without placing the entire file on-chain.&lt;/p&gt;

&lt;p&gt;For example, a system could be designed to verify a document against a previously recorded cryptographic reference. Whether that approach is suitable depends on the implementation and the required security properties.&lt;/p&gt;

&lt;p&gt;This is a general architectural pattern, not a claim about the specific implementation used in DocTrace.&lt;/p&gt;

&lt;p&gt;Exploring DocTrace&lt;/p&gt;

&lt;p&gt;SoluLab's DocTrace project provides a case study focused on blockchain and document management.&lt;/p&gt;

&lt;p&gt;You can review the DocTrace project case study to explore the documented project context.&lt;/p&gt;

&lt;p&gt;Practical lessons for blockchain developers&lt;/p&gt;

&lt;p&gt;Start with a clear problem statement.&lt;/p&gt;

&lt;p&gt;Decide what belongs on-chain and what belongs off-chain.&lt;/p&gt;

&lt;p&gt;Design permissions and key management early.&lt;/p&gt;

&lt;p&gt;Consider privacy and data-retention requirements.&lt;/p&gt;

&lt;p&gt;Test failure scenarios, not just successful transactions.&lt;/p&gt;

&lt;p&gt;Make verification understandable to the people using the application.&lt;/p&gt;

&lt;p&gt;Blockchain is one component of a larger system. The engineering challenge is to combine it with storage, identity, application logic, and a usable interface to meet a defined business need.&lt;/p&gt;

&lt;p&gt;What would you prioritize first in a blockchain document management project: integrity verification, access control, or integration with existing systems?&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>security</category>
      <category>web3</category>
    </item>
    <item>
      <title>GenAI + Tokenized RWAs: What Developers Should Consider Before Building</title>
      <dc:creator>Freddie Thompson</dc:creator>
      <pubDate>Wed, 21 Oct 2026 10:58:22 +0000</pubDate>
      <link>https://dev.to/freddie_thompson_e0488262/genai-tokenized-rwas-what-developers-should-consider-before-building-1h7o</link>
      <guid>https://dev.to/freddie_thompson_e0488262/genai-tokenized-rwas-what-developers-should-consider-before-building-1h7o</guid>
      <description>&lt;p&gt;Real-world asset (RWA) tokenization and generative AI solve different problems, but they can potentially complement each other in an application.&lt;/p&gt;

&lt;p&gt;Tokenization provides a digital framework for representing assets. Generative AI can help users interact with information through natural-language interfaces, summarization, and other information-processing tasks.&lt;/p&gt;

&lt;p&gt;The interesting engineering challenge is deciding where AI belongs in the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the Data Model
&lt;/h2&gt;

&lt;p&gt;Before introducing an AI component, identify the information the application manages.&lt;/p&gt;

&lt;p&gt;Depending on the project, this may include asset metadata, supporting documents, business rules, and transaction-related records. Each data source should have a clear owner, access policy, and level of authority.&lt;/p&gt;

&lt;p&gt;Do not assume that an AI-generated summary is equivalent to a verified asset record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the AI Component's Responsibility
&lt;/h2&gt;

&lt;p&gt;A useful design exercise is to separate informational tasks from authoritative operations.&lt;/p&gt;

&lt;p&gt;For example, an application might use AI to help users understand documentation. Any actual asset update or transaction would still need to follow the platform's established validation and authorization rules.&lt;/p&gt;

&lt;p&gt;This separation reduces the risk of treating generated content as an instruction that can execute without review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Think About Security Early
&lt;/h2&gt;

&lt;p&gt;When connecting AI to asset-related data, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which users can access each category of information?&lt;/li&gt;
&lt;li&gt;Can the model retrieve information the current user is not authorized to see?&lt;/li&gt;
&lt;li&gt;How will incorrect or unsupported answers be handled?&lt;/li&gt;
&lt;li&gt;Which actions require explicit approval?&lt;/li&gt;
&lt;li&gt;How can important operations be audited?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The right controls depend on the application's risk profile and architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate the Actual Use Case
&lt;/h2&gt;

&lt;p&gt;Before choosing a model or adding more infrastructure, define what success means.&lt;/p&gt;

&lt;p&gt;Possible evaluation criteria include answer accuracy, retrieval relevance, latency, task completion, and the rate of outputs requiring human correction. Select metrics based on the intended functionality rather than using them as generic performance claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Case Study to Explore
&lt;/h2&gt;

&lt;p&gt;SoluLab's &lt;a href="https://www.solulab.com/case-study/genai-in-tokenized-rwa-case-study/" rel="noopener noreferrer"&gt;GenAI in Tokenized RWA case study&lt;/a&gt; is relevant to developers investigating the intersection of generative AI and tokenized real-world assets.&lt;/p&gt;

&lt;p&gt;When reviewing the project, look for the documented use case, implemented features, technology choices, and evidence of outcomes. Avoid inferring a specific model, blockchain, token standard, or deployment architecture unless the source confirms it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The goal is not to put an LLM in every part of a tokenization stack. It is to identify a genuine information or workflow problem and determine whether AI can solve it without weakening the system's controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For developers, clear component boundaries are just as important as choosing the right technology.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
    <item>
      <title>Crypto Launchpad Development: Planning the Architecture Before Writing Code</title>
      <dc:creator>Freddie Thompson</dc:creator>
      <pubDate>Mon, 19 Oct 2026 11:11:53 +0000</pubDate>
      <link>https://dev.to/freddie_thompson_e0488262/crypto-launchpad-development-planning-the-architecture-before-writing-code-4lfj</link>
      <guid>https://dev.to/freddie_thompson_e0488262/crypto-launchpad-development-planning-the-architecture-before-writing-code-4lfj</guid>
      <description>&lt;p&gt;Crypto launchpad projects present an interesting engineering challenge: the application must support a defined product workflow while accounting for blockchain-specific behavior.&lt;/p&gt;

&lt;p&gt;Before implementing contracts or integrating a blockchain SDK, developers should understand the platform's functional requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Define the Requirements
&lt;/h2&gt;

&lt;p&gt;Start by documenting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User roles and permissions&lt;/li&gt;
&lt;li&gt;Project discovery and information requirements&lt;/li&gt;
&lt;li&gt;The intended token launch workflow&lt;/li&gt;
&lt;li&gt;On-chain versus off-chain responsibilities&lt;/li&gt;
&lt;li&gt;Transaction states and error scenarios&lt;/li&gt;
&lt;li&gt;Security and testing expectations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This requirement list becomes the basis for architectural decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Separate Application Concerns
&lt;/h2&gt;

&lt;p&gt;A typical design discussion may cover the following responsibilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Displays project information and guides users through the application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Manages application-specific logic and operational workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blockchain integration:&lt;/strong&gt; Handles communication with the selected network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart contracts:&lt;/strong&gt; Enforce required on-chain rules, if applicable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing and monitoring:&lt;/strong&gt; Help validate expected behavior and identify failures.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a general planning model, not a claim about the exact architecture used in the Token World project. The final design should follow the actual requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Design for Failure
&lt;/h2&gt;

&lt;p&gt;Blockchain interactions introduce scenarios that a conventional interface cannot simply ignore.&lt;/p&gt;

&lt;p&gt;Developers should plan for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rejected transactions&lt;/li&gt;
&lt;li&gt;Network interruptions&lt;/li&gt;
&lt;li&gt;Delayed transaction confirmation&lt;/li&gt;
&lt;li&gt;Invalid user inputs&lt;/li&gt;
&lt;li&gt;Unexpected contract responses&lt;/li&gt;
&lt;li&gt;Inconsistent application state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interface should communicate what happened and what the user can safely do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Make Security Part of Development
&lt;/h2&gt;

&lt;p&gt;Security review should cover the application and any on-chain components. Depending on the implementation, this may include permission checks, input validation, contract testing, and careful handling of transaction-related information.&lt;/p&gt;

&lt;p&gt;Avoid assuming that a successful deployment automatically means an application is secure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning From a Real Project
&lt;/h2&gt;

&lt;p&gt;SoluLab's &lt;a href="https://www.solulab.com/case-study/token-world-a-crypto-launchpad-platform/" rel="noopener noreferrer"&gt;Token World crypto launchpad case study&lt;/a&gt; offers a project reference for developers exploring crypto launchpad solutions.&lt;/p&gt;

&lt;p&gt;When evaluating a case study, look for documented requirements, technology choices, implementation details, and evidence of the resulting capabilities. These details are more useful for architecture planning than assumptions based on the project category alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;For blockchain developers, a successful launchpad project begins with clear requirements, a deliberate architecture, well-defined failure handling, and security-conscious engineering.&lt;/p&gt;

&lt;p&gt;Choose the technology stack after understanding the workflow it must support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Canonical URL:&lt;/strong&gt; Configure the original SoluLab URL as the canonical URL if DEV Community's current publishing interface supports it. Verify the setting before publishing.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Gold Tokenization: What Blockchain Developers Should Consider Before Building</title>
      <dc:creator>Freddie Thompson</dc:creator>
      <pubDate>Fri, 16 Oct 2026 10:35:00 +0000</pubDate>
      <link>https://dev.to/freddie_thompson_e0488262/gold-tokenization-what-blockchain-developers-should-consider-before-building-42k3</link>
      <guid>https://dev.to/freddie_thompson_e0488262/gold-tokenization-what-blockchain-developers-should-consider-before-building-42k3</guid>
      <description>&lt;p&gt;Gold tokenization sits at the intersection of smart contracts, asset management, and real-world operational processes.&lt;/p&gt;

&lt;p&gt;For developers, the interesting challenge is not just creating a token. It is designing a system in which the digital representation has a clearly defined relationship with the underlying asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define the Asset Model First
&lt;/h2&gt;

&lt;p&gt;Before choosing a token standard or writing a smart contract, establish what the token represents.&lt;/p&gt;

&lt;p&gt;Possible structures may involve direct ownership, a beneficial interest, or a contractual claim. Each structure has different legal and operational implications.&lt;/p&gt;

&lt;p&gt;Developers should not treat these models as interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Establish the Connection Between Physical and Digital Records
&lt;/h2&gt;

&lt;p&gt;A blockchain can record token issuance and transfers, but it cannot independently verify the existence or condition of physical gold.&lt;/p&gt;

&lt;p&gt;A complete design needs a defined process for verifying the underlying assets and reconciling the relevant records.&lt;/p&gt;

&lt;p&gt;Questions to resolve include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who verifies the gold?&lt;/li&gt;
&lt;li&gt;How are asset records maintained?&lt;/li&gt;
&lt;li&gt;What authorizes token issuance?&lt;/li&gt;
&lt;li&gt;How are discrepancies handled?&lt;/li&gt;
&lt;li&gt;What happens when an asset's status changes?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Design Token Controls Around Real Requirements
&lt;/h2&gt;

&lt;p&gt;Smart-contract controls should reflect the platform's documented operating model.&lt;/p&gt;

&lt;p&gt;Depending on the requirements, engineers may need to consider role-based permissions, controlled issuance, transfer restrictions, event logging, and secure administrative operations.&lt;/p&gt;

&lt;p&gt;These are potential design considerations, not claims about the specific implementation of any individual platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Treat Security and Operations as Part of the System
&lt;/h2&gt;

&lt;p&gt;Smart-contract security is only one layer of the overall solution. Operational procedures, access controls, supporting records, and incident-response plans also matter.&lt;/p&gt;

&lt;p&gt;A contract can execute exactly as written while the wider asset-management process still has weaknesses. This is why technical testing and operational validation should be considered together.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Review the Project Context
&lt;/h2&gt;

&lt;p&gt;SoluLab's &lt;a href="https://www.solulab.com/case-study/tangible-gold/" rel="noopener noreferrer"&gt;Tangible Gold case study&lt;/a&gt; is a relevant project reference for developers researching gold and digital asset tokenization.&lt;/p&gt;

&lt;p&gt;When reviewing the project, focus on the documented requirements, implemented functionality, technical decisions, and any explicitly reported outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;The key engineering lesson is to start with the asset and ownership model, then design the blockchain layer around it.&lt;/p&gt;

&lt;p&gt;A well-designed tokenization solution needs a consistent relationship between its digital records and the real-world rights those records are intended to represent.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>blockchain</category>
      <category>crypto</category>
      <category>web3</category>
    </item>
    <item>
      <title>Building a Tokenized Clean Energy Investment Platform: What Developers Should Consider</title>
      <dc:creator>Freddie Thompson</dc:creator>
      <pubDate>Thu, 15 Oct 2026 10:37:11 +0000</pubDate>
      <link>https://dev.to/freddie_thompson_e0488262/building-a-tokenized-clean-energy-investment-platform-what-developers-should-consider-55g2</link>
      <guid>https://dev.to/freddie_thompson_e0488262/building-a-tokenized-clean-energy-investment-platform-what-developers-should-consider-55g2</guid>
      <description>&lt;p&gt;Tokenization projects require more than smart contracts. When the underlying use case involves clean energy investment, developers must think about asset representation, transaction records, application security, and the relationship between digital tokens and real-world rights.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define the Asset Model First
&lt;/h2&gt;

&lt;p&gt;Before choosing a blockchain or token standard, establish what the platform represents.&lt;/p&gt;

&lt;p&gt;Questions to answer include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is the underlying asset?&lt;/li&gt;
&lt;li&gt;What rights does a token represent?&lt;/li&gt;
&lt;li&gt;Who verifies the asset information?&lt;/li&gt;
&lt;li&gt;How are changes to the underlying asset recorded?&lt;/li&gt;
&lt;li&gt;What happens when an asset or investment arrangement changes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These decisions influence the data model, application logic, and contract design.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Design the Tokenization Workflow
&lt;/h2&gt;

&lt;p&gt;A conceptual workflow may include asset onboarding, verification, token issuance, investor participation, and transaction record management.&lt;/p&gt;

&lt;p&gt;Each step needs defined permissions, validation rules, error handling, and auditability. The actual workflow depends on the project's legal and technical requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Treat Blockchain as One Layer
&lt;/h2&gt;

&lt;p&gt;Blockchain can provide transaction records and programmable rules, where appropriate. The broader application may also require conventional backend services, interfaces, data storage, monitoring, and identity or access-management controls.&lt;/p&gt;

&lt;p&gt;The architecture should reflect actual business requirements instead of introducing blockchain components without a clear purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Account for Security and Compliance
&lt;/h2&gt;

&lt;p&gt;Developers should consider contract testing, authorization, secure key management, application vulnerabilities, and transaction monitoring.&lt;/p&gt;

&lt;p&gt;The team must also establish how digital tokens relate to enforceable investment rights. Technical implementation alone cannot establish legal ownership or guarantee compliance.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real-World Project to Explore
&lt;/h2&gt;

&lt;p&gt;SoluLab's &lt;a href="https://www.solulab.com/case-study/tokenized-clean-energy-investment-platform/" rel="noopener noreferrer"&gt;Tokenized Clean Energy Investment Platform case study&lt;/a&gt; offers a relevant project reference for developers interested in the intersection of clean energy investment and tokenization.&lt;/p&gt;

&lt;p&gt;Review the original project details before drawing conclusions about its specific blockchain, architecture, or implemented features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;The most important development decision is not simply which token standard to use. It is defining the asset model, investment rights, workflow, and security requirements first.&lt;/p&gt;

&lt;p&gt;A well-designed tokenization platform connects these elements into a coherent system rather than treating token issuance as the entire solution.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Context Propagation Across gRPC and REST: One Broken Hop Splits Every Trace</title>
      <dc:creator>Freddie Thompson</dc:creator>
      <pubDate>Mon, 05 Oct 2026 07:50:06 +0000</pubDate>
      <link>https://dev.to/freddie_thompson_e0488262/context-propagation-across-grpc-and-rest-one-broken-hop-splits-every-trace-2j9k</link>
      <guid>https://dev.to/freddie_thompson_e0488262/context-propagation-across-grpc-and-rest-one-broken-hop-splits-every-trace-2j9k</guid>
      <description>&lt;p&gt;Your trace shows api-gateway calling orders, then stops. orders definitely called inventory, but inventory appears as a separate, parentless trace.&lt;/p&gt;

&lt;p&gt;The context did not propagate. It happens at four specific boundaries, and each needs a different fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundary 1: protocol translation
&lt;/h2&gt;

&lt;p&gt;W3C Trace Context travels as a &lt;code&gt;traceparent&lt;/code&gt; HTTP header. gRPC has metadata, not headers, and the mapping is not automatic unless an instrumented interceptor does it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;grpc&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;otel_context&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.trace&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SpanKind&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_MetadataSetter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# gRPC metadata keys MUST be lowercase. A capitalised key is
&lt;/span&gt;        &lt;span class="c1"&gt;# silently dropped by some implementations and raises in others,
&lt;/span&gt;        &lt;span class="c1"&gt;# which is a fun way to lose context on one language's client only.
&lt;/span&gt;        &lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_MetadataGetter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;carrier&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;carrier&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[])]&lt;/span&gt;


&lt;span class="n"&gt;_SETTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_GETTER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_MetadataSetter&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;_MetadataGetter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;_tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TracingClientInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnaryUnaryClientInterceptor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;intercept_unary_unary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_call_details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;client_call_details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SpanKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLIENT&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_call_details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
            &lt;span class="n"&gt;propagate&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="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_SETTER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;new_details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client_call_details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;continuation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rpc.grpc.status_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TracingServerInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServerInterceptor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;intercept_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handler_call_details&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler_call_details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invocation_metadata&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt;
            &lt;span class="n"&gt;getter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_GETTER&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;otel_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;continuation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler_call_details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Detaching is not optional. A leaked context attaches itself to
&lt;/span&gt;            &lt;span class="c1"&gt;# whatever request the worker thread handles next, and you get
&lt;/span&gt;            &lt;span class="c1"&gt;# spans from request B parented under request A.
&lt;/span&gt;            &lt;span class="n"&gt;otel_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lowercase-metadata rule is worth internalising. It fails asymmetrically — a Go client talking to a Python server may work while the reverse does not — which makes it look like a language bug rather than a casing bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundary 2: message queues
&lt;/h2&gt;

&lt;p&gt;A trace that crosses Kafka or SQS has a &lt;strong&gt;producer&lt;/strong&gt; and a &lt;strong&gt;consumer&lt;/strong&gt; that may run hours apart. Parent-child is the wrong relationship; use a &lt;strong&gt;link&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.trace&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NonRecordingSpan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SpanContext&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publish &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SpanKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PRODUCER&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;propagate&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="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_SETTER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parent_ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;propagate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt;
        &lt;span class="n"&gt;getter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_GETTER&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;parent_span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent_ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# A link, not a parent. The consumer may run long after the producer's
&lt;/span&gt;    &lt;span class="c1"&gt;# trace ended; making it a child produces traces with multi-hour gaps
&lt;/span&gt;    &lt;span class="c1"&gt;# that break every latency percentile you compute from span duration.
&lt;/span&gt;    &lt;span class="n"&gt;links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="n"&gt;sc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parent_span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_span_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;process &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SpanKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CONSUMER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;links&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messaging.kafka.partition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messaging.kafka.offset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Boundary 3: thread pools and async
&lt;/h2&gt;

&lt;p&gt;Context is stored in a context-local. Hand work to a thread pool and the new thread has a fresh, empty one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;contextvars&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContextPreservingExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Copies the caller&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s context into the worker thread.

    Without this, every span produced inside a pool task is a new root and
    your trace loses everything that happens in a background worker - which
    is usually the slow part you were trying to see.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contextvars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;asyncio.create_task&lt;/code&gt; copies context automatically. &lt;code&gt;loop.run_in_executor&lt;/code&gt; and raw &lt;code&gt;Thread&lt;/code&gt; do not. Neither does most third-party library code that manages its own pool, which is why a trace can break inside a database driver you did not write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundary 4: sampling decisions
&lt;/h2&gt;

&lt;p&gt;If service A samples a trace out and service B makes its own independent decision, you get a child span with no parent. Always use &lt;strong&gt;ParentBased sampling&lt;/strong&gt; so the root’s decision is authoritative downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the broken hop
&lt;/h2&gt;

&lt;p&gt;Do not hunt for these manually. Query your tracing backend for &lt;strong&gt;spans with a parent ID that does not resolve&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_orphans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Group orphaned spans by service. The service that shows up is the
    one RECEIVING broken context - so the bug is in its caller, or in its
    own extract path. Run this weekly; it catches a regression the day a
    new service ships without the interceptor.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;span_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;orphans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parent_span_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;known&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;svc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;orphans&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orphans&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="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;orphans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&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;p&gt;Caveat: a parent can be legitimately missing because it was sampled out or is still in flight. Run this over a completed window and treat a &lt;em&gt;rate&lt;/em&gt; as the signal, not individual cases. A service that jumps from 2% orphans to 60% after a deploy has a propagation bug; one that sits steadily at 3% probably has sampling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Propagate more than trace context
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;traceparent&lt;/code&gt; gets you the trace. &lt;strong&gt;Baggage&lt;/strong&gt; carries application context — tenant ID, feature flag cohort, request priority — to every downstream service without threading it through every function signature.&lt;/p&gt;

&lt;p&gt;Two warnings. Baggage travels in headers to every hop, including third parties, so it must never contain anything sensitive. And it costs bytes on every call; a few small keys are fine, a serialised user object is not.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;We build distributed backend systems at SoluLab — more on our &lt;a href="https://www.solulab.com/custom-software-development-company/" rel="noopener noreferrer"&gt;custom software development&lt;/a&gt; work.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>backend</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Multi-Tenant Database Isolation: RLS, Schema-per-Tenant, and the Migration Wall</title>
      <dc:creator>Freddie Thompson</dc:creator>
      <pubDate>Mon, 05 Oct 2026 07:47:25 +0000</pubDate>
      <link>https://dev.to/freddie_thompson_e0488262/multi-tenant-database-isolation-rls-schema-per-tenant-and-the-migration-wall-4og0</link>
      <guid>https://dev.to/freddie_thompson_e0488262/multi-tenant-database-isolation-rls-schema-per-tenant-and-the-migration-wall-4og0</guid>
      <description>&lt;p&gt;Three ways to keep tenants apart in one Postgres cluster:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discriminator column&lt;/strong&gt; — &lt;code&gt;tenant_id&lt;/code&gt; on every table, filtered in application code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row-level security&lt;/strong&gt; — &lt;code&gt;tenant_id&lt;/code&gt; plus RLS policies the database enforces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema-per-tenant&lt;/strong&gt; — each tenant gets its own set of tables.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Option 1 is not isolation. It is a convention, and it fails the first time someone writes a query without the filter. That is not hypothetical — it is one of the most common sources of cross-tenant data exposure, and it happens in a reporting endpoint or an admin tool rather than the main code path, which is why review misses it.&lt;/p&gt;

&lt;p&gt;The real choice is between 2 and 3, and it comes down to a trade most teams discover too late: &lt;strong&gt;RLS scales with tenant count, schema-per-tenant scales with tenant size.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Row-level security, done correctly
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="n"&gt;ENABLE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="k"&gt;FORCE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- applies to the table owner too&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;tenant_isolation&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;
    &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.tenant_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.tenant_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things in there that are load-bearing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FORCE ROW LEVEL SECURITY.&lt;/strong&gt; Without it, the table owner bypasses the policy entirely. If your application connects as the owner — which it does by default in most setups — RLS is enabled and doing absolutely nothing. This is the single most common way an RLS deployment provides no protection while appearing to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WITH CHECK as well as USING.&lt;/strong&gt; USING filters reads. Without WITH CHECK, a tenant can &lt;em&gt;insert&lt;/em&gt; a row with someone else’s tenant_id. You get read isolation and write contamination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The true second argument to current_setting.&lt;/strong&gt; Makes it return NULL instead of erroring when unset. That sounds convenient and is the trap — tenant_id = NULL is NULL, which is not true, so the policy denies everything. Failing closed is right; just know that an unset variable produces “no rows” rather than an error, which is a confusing thing to debug.&lt;/p&gt;

&lt;p&gt;And the part that actually enforces it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;contextmanager&lt;/span&gt;


&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tenant_scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    SET LOCAL is scoped to the transaction and cannot leak into the next
    checkout from the pool. Plain SET persists on the connection - with
    PgBouncer in transaction mode that means the next tenant inherits it,
    which is a cross-tenant read with no bug in your query.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET LOCAL app.tenant_id = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;


&lt;span class="c1"&gt;# The role matters as much as the policy.
# CREATE ROLE app_user NOLOGIN;
# GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
# -- app_user is NOT the table owner, so FORCE is not the only thing
# -- standing between you and a full-table read.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SET LOCAL versus SET is the whole connection-pooling story. Get it wrong and the failure is intermittent, load-dependent, and cross-tenant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each model breaks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;RLS breaks on the query planner.&lt;/strong&gt; Policies are predicates, and they interact with indexes in ways that are not always obvious. A composite index on &lt;code&gt;(tenant_id, created_at)&lt;/code&gt; is essential; without it the planner may filter after scanning. Worse, RLS can prevent certain plan shapes entirely, and a query that is fast for a small tenant becomes a sequential scan for a large one. Check EXPLAIN output &lt;em&gt;as a tenant&lt;/em&gt;, not as a superuser, or you are reading a plan the application will never get.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema-per-tenant breaks on migrations.&lt;/strong&gt; At 4,000 schemas, ALTER TABLE runs 4,000 times. A migration that takes 8 seconds per schema is nine hours, and it is not atomic — you will be half-migrated when something fails, with application code that has to tolerate both shapes. Add connection-level catalog bloat: Postgres’s system catalogs grow with schema count, and planning time degrades measurably in the thousands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both break on cross-tenant queries.&lt;/strong&gt; Admin dashboards, usage billing, aggregate analytics. Under RLS you need a separate role that bypasses policies, which is a privileged path you must guard as carefully as the policies themselves. Under schema-per-tenant you need a UNION across thousands of schemas, which is not viable, so you end up with a separate analytics store and a pipeline to keep it current.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision, concretely
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use RLS when&lt;/strong&gt; you have many small tenants (SaaS, self-serve), you need one migration to cover everyone, and no tenant demands physical separation. This is most products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use schema-per-tenant when&lt;/strong&gt; tenants are few and large, they have genuinely different schemas (custom fields as real columns), or a contract requires demonstrable physical separation. Enterprise and regulated deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a separate database or cluster when&lt;/strong&gt; a tenant’s contract requires it, or when one tenant’s load would otherwise degrade everyone. This is the honest answer for a handful of large accounts and it is fine to run a hybrid: RLS for the long tail, dedicated clusters for the top ten. The routing layer that picks the connection is a small amount of code and it saves you from designing for the worst case everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing isolation, which is the part that gets skipped
&lt;/h2&gt;

&lt;p&gt;Isolation is a security property, so test it like one — adversarially, in CI, on every build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ALL_TENANT_TABLES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_no_cross_tenant_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;tenant_scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TENANT_A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT tenant_id FROM &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: tenant A should see its own rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TENANT_A&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: cross-tenant leak&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ALL_TENANT_TABLES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_cannot_write_other_tenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;tenant_scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TENANT_A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (tenant_id, ...) VALUES (%s, ...)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TENANT_B&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_every_tenant_table_has_rls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Catches the new table someone added without a policy. This test is
    worth more than the other two combined, because the failure it prevents
    is the one that actually happens.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT c.relname FROM pg_class c
        JOIN pg_namespace n ON n.oid = c.relnamespace
        WHERE n.nspname = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;public&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; AND c.relkind = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
          AND c.relname = ANY(%s)
          AND (NOT c.relrowsecurity OR NOT c.relforcerowsecurity)
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ALL_TENANT_TABLES&lt;/span&gt;&lt;span class="p"&gt;,)).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tables without forced RLS: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That third test is the important one. RLS is opt-in per table, so the realistic failure is not a broken policy — it is a table shipped six months from now by someone who did not know the convention. A catalog assertion in CI catches it; code review does not.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We build multi-tenant platforms where this decision gets made early and lived with for years — the &lt;a href="https://www.solulab.com/case-study/medicgraph-patient-management-software/" rel="noopener noreferrer"&gt;MedicGraph patient management case study&lt;/a&gt; is one of them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>postgres</category>
      <category>security</category>
    </item>
    <item>
      <title>Zero-Downtime Database Migration: The Dual-Write Window Is Where It Goes Wrong</title>
      <dc:creator>Freddie Thompson</dc:creator>
      <pubDate>Mon, 05 Oct 2026 07:44:24 +0000</pubDate>
      <link>https://dev.to/freddie_thompson_e0488262/zero-downtime-database-migration-the-dual-write-window-is-where-it-goes-wrong-5dm1</link>
      <guid>https://dev.to/freddie_thompson_e0488262/zero-downtime-database-migration-the-dual-write-window-is-where-it-goes-wrong-5dm1</guid>
      <description>&lt;p&gt;The standard playbook for moving a table to a new store is four phases: &lt;strong&gt;dual-write, backfill, verify, and cut over&lt;/strong&gt;. Every phase has a failure mode that produces silent, permanent divergence, and the order the phases run in is load-bearing.&lt;/p&gt;

&lt;p&gt;The one that catches most teams: &lt;strong&gt;if you backfill before dual-writing, you lose every row written during the backfill.&lt;/strong&gt; The backfill reads a snapshot; concurrent writes land in the old store only; you finish, see matching row counts, and ship. The missing rows are the ones written during the window, which is exactly your busiest data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dual-write must be live before the backfill starts, not after.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: Dual-Write, Old Store Authoritative
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Protocol&lt;/span&gt;

&lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Phase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;OLD_ONLY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old_only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;DUAL_OLD_AUTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dual_old_auth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# write both, read old, compare async
&lt;/span&gt;    &lt;span class="n"&gt;DUAL_NEW_AUTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dual_new_auth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# write both, read new, old is fallback
&lt;/span&gt;    &lt;span class="n"&gt;NEW_ONLY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new_only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MigrationRouter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt;
    &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Phase&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;Phase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NEW_ONLY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="c1"&gt;# Authoritative store first, and a failure there propagates.
&lt;/span&gt;        &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shadow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Phase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OLD_ONLY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Phase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DUAL_OLD_AUTH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;Phase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OLD_ONLY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="c1"&gt;# Shadow write failures must NOT fail the request. If they do, the
&lt;/span&gt;        &lt;span class="c1"&gt;# migration has coupled your availability to the store you are still
&lt;/span&gt;        &lt;span class="c1"&gt;# testing - and you will roll back a migration that was fine.
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;shadow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;incr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migration.shadow_write_failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shadow write failed key=%s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Phase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OLD_ONLY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Phase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DUAL_OLD_AUTH&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;Phase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DUAL_NEW_AUTH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Fallback during cutover. Every hit here is a real gap - alert on it.
&lt;/span&gt;            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;incr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migration.new_store_miss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two decisions in there matter more than they look.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Swallow and Count Shadow-Write Failures
&lt;/h3&gt;

&lt;p&gt;Shadow-write failures are swallowed and counted. The instinct is to fail loudly. &lt;strong&gt;Do not&lt;/strong&gt; — you have just made your production availability depend on the store you are still validating.&lt;/p&gt;

&lt;p&gt;Count them, alert on the rate, and let the reconciler repair them.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Alert on &lt;code&gt;new_store_miss&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;new_store_miss&lt;/code&gt; gets an alert, not a log line. Once you flip to new-authoritative, every fallback read is a row that should be in the new store and is not.&lt;/p&gt;

&lt;p&gt;If that counter is non-zero, you are not ready to cut over. This is the single metric that tells you so.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 2: Backfill With a Stable Cursor
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;backfill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MigrationRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;source_scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Copy historical rows.

    MUST run with dual-write already live.

    Uses a keyset cursor, not OFFSET:
    OFFSET pagination over a table taking concurrent writes skips
    and duplicates rows as pages shift underneath it.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;copied&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;source_scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Never overwrite. A live dual-write may have already put a NEWER
&lt;/span&gt;            &lt;span class="c1"&gt;# value here; the backfill is reading an older snapshot. Clobbering
&lt;/span&gt;            &lt;span class="c1"&gt;# it silently reverts a customer's write.
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;copied&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;

        &lt;span class="c1"&gt;# Crude throttle; prefer a token bucket keyed on replica lag
&lt;/span&gt;        &lt;span class="c1"&gt;# so the backfill self-limits.
&lt;/span&gt;        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;copied&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;if existing is None&lt;/code&gt; guard is the second classic corruption.&lt;/p&gt;

&lt;p&gt;A backfill that blindly writes will overwrite fresher dual-written values with stale snapshot data, and the damage is invisible until a customer reports that an edit reverted.&lt;/p&gt;

&lt;p&gt;For rows that genuinely need updating, you need a &lt;strong&gt;version or &lt;code&gt;updated_at&lt;/code&gt; comparison&lt;/strong&gt; rather than a presence check.&lt;/p&gt;

&lt;p&gt;The source of truth for that comparison has to be a column the application actually maintains, not a database-generated timestamp that the backfill itself bumps.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3: Verify Continuously, Not Once
&lt;/h2&gt;

&lt;p&gt;A one-shot comparison at the end tells you nothing about steady state. Sample continuously through the whole dual-write period:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sample_and_compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MigrationRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;old_val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;new_val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;checked&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_val&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;old_val&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;old_val&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;new_val&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_val&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mismatched&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;samples&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mismatches&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect a non-zero mismatch rate from timing alone — you read the two stores microseconds apart, and a write can land between those reads.&lt;/p&gt;

&lt;p&gt;Re-check a mismatch after a short delay before counting it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A mismatch that persists is real.&lt;/li&gt;
&lt;li&gt;A mismatch that resolves is replication lag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Failing to make that distinction produces an alert channel everyone learns to ignore, which is worse than no alerting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 4: Cut Over and Keep the Way Back
&lt;/h2&gt;

&lt;p&gt;Flip:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;DUAL_OLD_AUTH → DUAL_NEW_AUTH&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Do this behind a &lt;strong&gt;runtime flag, not a deploy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You want the rollback to be a configuration change measured in seconds because the thing you are protecting against is a failure that shows up under production load and could not be reproduced in staging.&lt;/p&gt;

&lt;p&gt;Keep dual-write running for at least &lt;strong&gt;one full business cycle&lt;/strong&gt; after the flip — a week, a month-end, or whatever your longest periodic job is.&lt;/p&gt;

&lt;p&gt;The rollback path only exists while the old store is still current.&lt;/p&gt;

&lt;p&gt;The moment you stop writing to it, &lt;code&gt;NEW_ONLY&lt;/code&gt; is irreversible. That step should therefore be a separate, deliberate, scheduled change rather than the tail end of the cutover.&lt;/p&gt;

&lt;h1&gt;
  
  
  What Makes This Genuinely Hard
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Transactions That Span Both Stores
&lt;/h2&gt;

&lt;p&gt;If a single logical operation writes to two tables and only one has migrated, you have lost atomicity for the duration of the migration.&lt;/p&gt;

&lt;p&gt;There is no clean fix.&lt;/p&gt;

&lt;p&gt;The options are to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrate the whole transactional unit together.&lt;/li&gt;
&lt;li&gt;Accept a reconciliation job for the migration window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick deliberately rather than discovering the problem mid-migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema Changes During the Migration
&lt;/h2&gt;

&lt;p&gt;Someone will want to add a column while you are three weeks into a dual-write.&lt;/p&gt;

&lt;p&gt;The migration router has to tolerate the change on both sides. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The new store's schema has to remain a superset.&lt;/li&gt;
&lt;li&gt;The write path has to tolerate unknown fields.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reads That Are Not Key-Value
&lt;/h2&gt;

&lt;p&gt;Everything above assumes point reads.&lt;/p&gt;

&lt;p&gt;Range queries, aggregates, and joins do not dual-read cleanly.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;COUNT(*)&lt;/code&gt; disagreeing between stores during a backfill is expected, not necessarily a bug. Any verification built on aggregates will therefore produce noise for the entire migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;A zero-downtime database migration is not simply a matter of copying data and switching traffic.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;dual-write window is the critical safety mechanism&lt;/strong&gt;. Start dual-writing before the backfill, protect newer writes from stale backfill data, verify continuously, monitor fallback reads, and keep the old store writable long enough to preserve a real rollback path.&lt;/p&gt;

&lt;p&gt;We do data platform and legacy modernization work at &lt;strong&gt;SoluLab&lt;/strong&gt; — the &lt;strong&gt;InfuseNet case study&lt;/strong&gt; covers one of these migrations end to end.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
