
Production Ready ( Quality, performance, and the lessons learned shipping to 150 stores )
Got a project?
Let's discuss your project
THE JOURNEY SO FAR: We chose dbt over custom scripts. Built observability with freshness checks and Elementary. Optimized with incremental models and snapshots. Scaled with macros and repeatable patterns. Now it's time to ship to production and learn what that really means.
The First Production Failure
2am on a Wednesday
The page went out: "Critical data quality failure in production." Ten thousand orders from our biggest store showed NULL for customer IDs. The relationships test failed. Marketing's morning campaign was blocked. Revenue reports were broken.
Root cause? The source system changed a column from customer_id to customerId (camelCase). Our staging model still looked for customer_id. It found nothing. We loaded nulls.
Lesson: Tests saved us from shipping bad data downstream, but we should have caught the schema change earlier. We added dbt source freshness checks and schema change alerts to Elementary that night.
Production teaches lessons that development can't. Here's what we learned.
Data Quality: Tests Are Not Optional
In development, tests feel like extra work. In production, they're the difference between catching problems at 2am versus explaining them to the CEO at 9am.
The Testing Hierarchy
not_null - Primary keys, foreign keys, required business fields. If it should always have a value, test it.
unique - Order IDs, customer IDs, any identifier. Duplicates cause double-counting and broken joins.
relationships - Referential integrity. Every order_items.order_id must exist in orders.order_id.
Custom Tests - Business rules. "Revenue can't be negative." "Order dates can't be in the future." Domain-specific validations.
Here's what our test configuration looks like for a critical mart:
models:- name: mart_daily_store_salescolumns:- name: order_datetests:- not_null- dbt_utils.expression_is_true:expression: "<= current_date()"- name: store_idtests:- not_null- relationships:to: ref('dim_stores')field: store_id- name: total_revenuetests:- not_null- dbt_utils.expression_is_true:expression: ">= 0"
These tests run after every dbt run. If any fail, the job fails. We catch bad data before it reaches dashboards.
Performance: The Optimization Journey
Our first full production run took six hours. After optimization, it takes 22 minutes. Here's how we got there.
BEFORE OPTIMIZATION
6h Full refresh on every table, every run
AFTER OPTIMIZATION
22m Incremental + selective + parallel
Strategy 1: Incremental Models
We covered this in Episode 2, but it's worth repeating: incremental models were our biggest performance win. Facts tables like orders and order_items process only changes, not the full history.
Strategy 2: Selective Runs with State
In development, we rarely need to rebuild everything. The --select flag with state comparison is magic:
# Only run models that changed since last production rundbt run --select state:modified+# Only run a specific mart and its dependenciesdbt run --select +mart_daily_store_sales# Only run models tagged 'daily'dbt run --select tag:daily
For scheduled jobs, we run modified models on code deploys and tagged models on schedules. Daily jobs run tag:daily. Weekly aggregations run tag:weekly.
Strategy 3: Parallelism
dbt can run independent models in parallel. We configured threads: 8 in our profile. Models at the same layer (e.g., all staging models) run simultaneously, limited only by database capacity.
Watching eight staging models turn green at once never gets old.
Strategy 4: Indexes on Large Tables
We add indexes in post_hook configurations on columns used for filtering and joining:
{{ config(materialized='table',schema=var('target_schema'),post_hook=["alter table {{ this }}add index idx_store_date (store_id, order_date)","alter table {{ this }}add index idx_status (order_status)"]) }}
Query performance on these tables improved 10x for common filters.
Troubleshooting: The Debug Toolkit
When things break in production (and they will), here's the toolkit that saves us:
dbt compile - Inspect the actual SQL dbt will run. Check for variable substitutions, macro expansions, Jinja logic. 90% of "it doesn't work" is "the SQL isn't what I thought."
dbt debug - Verify connections, profiles, and configurations. When a model won't run, start here to eliminate environmental issues.
Elementary dashboards - See which models failed, which tests failed, and trends over time. "This test started failing three days ago" is a powerful diagnostic.
Let's Build Something Great Together
Ready to transform your idea into a powerful software solution? Talk to our experts and get a free consultation.
Contact UsLogs and lineage - logs/dbt.log contains every query dbt ran. The lineage graph in documentation shows impact: if stg_orders fails, what downstream models are blocked?
Manual query validation - When all else fails, copy the compiled SQL from target/compiled/, run it manually in MySQL, and see the actual error message.
Hard-Won Lessons
The MySQL Schema Enforcement Saga
We wanted to use dbt's contract: {enforced: true} to validate column types match our schema definitions. On MySQL, we hit limitations: not all constraints are supported, and we needed custom indexes and foreign keys.
After three failed attempts, we settled on contract: {enforced: false} plus ALTER TABLE post-hooks to enforce the schema we needed.
Lesson: Don't fight your database's limitations. Use the tools dbt provides (contracts, tests, docs), but augment with database-specific solutions when needed. Pragmatism beats purity.
The Variable Naming Incident
We had source_schema and target_schema as variables. A developer added source_db for database name. They didn't update the README. Three people spent four hours debugging why models wouldn't compile before discovering the missing variable.
Lesson: Document your variables in dbt_project.yml with defaults and descriptions. Make --vars usage obvious in your README. Future teammates (including future you) will thank you.
What We Built: A Production-Ready Framework
The Results
• 150 stores processed with a single dbt project
• 22-minute runs processing millions of daily transactions
• 200+ data quality tests protecting every critical model
• Zero manual intervention for routine runs (though we monitor everything)
• Complete lineage from source tables to executive dashboards
• Historical snapshots preserving customer and product changes over time
• Automatic alerts for freshness, test failures, and schema changes
More importantly, we built a team culture around data quality. Tests aren't optional. Documentation isn't an afterthought. Pull requests include SQL review. Everyone understands the lineage of their models.
New engineers onboard in days instead of weeks because the project is self-documenting. We add new business entities (promotions, returns, inventory) by following the same pattern every time.
The Philosophy That Emerged
What We'd Tell Ourselves a Year Ago
Start Simple - Don't build for 150 stores on day one. Build for three stores really well. Patterns that work at small scale guide scaling decisions.
Test Everything - If it matters, test it. Primary keys, foreign keys, business rules. Tests are documentation that runs. They're proof your data is correct.
Document as You Go - Future you won't remember why you made that choice. Present you should write it down in the model's description field.
Macros Save Time - But write them when you need them, not because they're cool. Premature abstraction is as bad as copy-paste.
Observability Isn't Optional - You can't fix what you can't see. Freshness checks, test monitoring, and alerting should launch with your first production run.
Incremental > Perfect - Ship the staging models. Ship basic tests. Ship documentation with TODOs. Iterate in production beats perfecting in development.
The End of the Beginning
This blog series documented our journey from retail data chaos to a production-grade dbt pipeline. We migrated 150 store databases into a unified analytics warehouse. We built observability, performance, quality, and scalability into every layer.
But honestly? We're still learning. Every new business requirement teaches us something. Every production incident makes the system stronger. Every new team member brings fresh perspectives.
dbt gave us the framework. We built the practices, patterns, and culture on top of it. That combination good tools plus thoughtful practices is what makes data engineering sustainable at scale.
Series Complete
Thanks for following along on this journey from chaos to production-ready data pipelines.
The dbt Migration Chronicles · Episode 4 of 4 - Series Finale
Written for data teams who learned these lessons the hard way, so you don't have to

AI Writes the Code • Stop Blindly Shipping It Without Reviewing the Engineering
A practical perspective on AI-generated code, scalability, maintainability, and engineering judgment and why engineering review still owns the final decision.
Read More
Migrating Databases with AWS DMS With Nearly Zero Downtime
Learn how to migrate databases using AWS DMS with nearly zero downtime using Full Load, CDC, validation, monitoring, and controlled cutover.
Read More
How to Add LiveKit Video Calling to a Next.js App
Add embedded video & audio calling to Next.js with LiveKit Cloud. Compared vs Twilio, Daily, Agora, Zoom — plus token auth, guests & recording.
Read More
We chose ECS over EKS: what we gained and what we gave up
An honest comparison of ECS vs EKS the costs, tradeoffs, and real-world reasoning behind choosing ECS for a production platform on AWS.
Read More
Upgrading Legacy Systems: From Outdated Technology to Competitive Advantage
Learn how to upgrade legacy systems through application modernization, API integration, cloud migration, security improvements, and incremental system upgrades without disrupting business operations.
Read More
Building Distributed Tracing and Observability with AWS X-Ray
A practical guide to correlating requests across a multi-tier application using correlation IDs, AWS X-Ray segments, and structured logging for faster incident debugging.
Read More
Designing Before and After AI: What Really Changed
A look at how AI has transformed UI/UX design from manual wireframes and slow research to AI-assisted prototyping, design-to-code, and personalization at scale.
Read More
Beyond Prompting: Managing Context and Tokens in AI Coding Tools
Ever wondered why your AI coding agent starts losing context or hits a hard limit mid-task? The answer lies in tokens and the context window. Good AI coding is not about giving the model the most information. It is about giving it the right information at the right time.
Read More
What Is llms.txt? How It Helps Google, AI Search, and Agentic Browsing Find Your Website
Learn what llms.txt is, how it differs from sitemap.xml and robots.txt, and how it can help your site get found by Google, AI search tools, and AI agents.
Read More