This is a series of articles about database version control and database-as-code (GitOps)
- What is Database Version Control?
- Database Version Control, State-based or Migration-based? (this one)
- Database as Code - the Good, the Bad and the Ugly
- The Database as Code Landscape
- Database Version Control Best Practice
In Infrastructure as Code (IaC) space, the state-based approach has become the de-facto standard. By contrast, in Database as Code (DaC) space, teams still prefer the migration-based approach to manage their database schemas. Below we give an overview of these 2 approaches and dive into the rationale behind the industry divergence. We also look at what changed since 2021, now that Atlas has brought state-based to DaC and Bytebase itself supports state-based schema change for Postgres. In the end, we show how Bytebase gets the best of both worlds.
Infrastructure as Code (IaC) and Database as Code (DaC) both belong to Configuration as Code (CaC), and there are 2 different approaches to manage the configuration files:
- State-based version control (declarative)
- Migration-based version control (imperative)
State-based version control (declarative)
State-based approach stores the desired end state of the entire schema in the code repository. For MySQL, it means to store the schema dump created by mysqldump.
It's worth mentioning in IaC, popular systems like Kubernetes, HashiCorp, and Terraform all adopt this approach.
Migration-based version control (imperative)
Migration-based approach stores the migration scripts in the repository. Each script contains a set of DDL statements such as CREATE/ALTER/DROP TABLE. The desired schema state is achieved by executing each of those scripts in a deterministic order.
Migration-based approach is more intuitive since this is how we change things in normal life.
It's also worth mentioning in IaC, before the Kubernetes, Terraform era, this was the way every team managed their infra: a bunch of shell scripts containing imperative commands to provision the resources.
For Infrastructure as Code, the industry has already shifted to state-based approach
State-based IaC delivers a couple of key benefits:
- Keep a straightforward single source of truth (SSOT) in the repository. Configuration is represented by a single source file. For migration-based approach, people need to derive the SSOT from many migration files.
- Usability. It's simpler to describe the end state without worrying about the ordering dependency. It's the Kubernetes/Terraform backend that does the heavy lifting to reconcile the system to the desired state.
Also tools like Kubernetes, Terraform serve as the catalyst for the adoption.
For Database as Code, migration-based approach is still the mainstream
Unlike its counterpart, teams still prefer the migration-based approach to manage database schema. On one hand, this is due to the supply part.
- Most existing tooling around code-based schema management favors migration-based approach.
- Same with the popular application frameworks, they all use the Up/Down method to manage schema change/rollback. This is a variation to write migration in a specific programming language instead of raw SQL.
When I first wrote this in 2021, no state-based system had gained popularity in the database domain over the previous 5 years the way Kubernetes and Terraform did for IaC. That has finally started to change. Atlas (GA in 2022, at v1.2 as of May 2026) brings the Terraform-style "plan then apply" loop to schema on top of HCL and SQL, and SchemaHero takes a Kubernetes-native route with CRDs that an operator reconciles. So state-based DaC is no longer vaporware.
And yet migration-based is still what most teams run in production. Why hasn't state-based taken over the database domain the way it took over IaC? I attribute it to a couple of reasons.
Lack of system support
State-based or migration-based describes the view from the user's perspective. If we look from the system's perspective, the state-based approach requires much more engineering effort to get right.
Kubernetes has the built-in controller pattern to support the state-based approach from day 1. That's not the case for database systems. Neither MySQL nor PostgreSQL has any built-in feature to support the state-based approach like Kubernetes does, and that's still true in 2026. Atlas and SchemaHero work around it from the outside, in an external tool, which is impressive but also tells you how much harder it is when the engine itself gives you nothing to build on.
Managing data (stateful) resources is much more complex than managing computing/networking (stateless) resources
You may wonder, HashiCorp Terraform also faces a similar problem since the infrastructure it manages lives in public cloud providers, which are black boxes.
Well, first, that's why Terraform is worth its hype :) It has done a terrific job of handling the complexity of reconciling between the user's desired infrastructure state and the cloud provider's actual state under the hood.
Another reason is the state-based approach is inherently a fit to manage ephemeral computing/networking resources since they can be destroyed and rebuilt. Database holds data (the state). To deliver a complete state-based approach for DaC, it not only needs to solve the schema (metadata) reconciliation problem like its IaC peer, but also requires dealing with the data.
Teams have better control using migration-based approach than state-based approach, and the cost of database mistakes is too high
Migration-based approach uses step-by-step commands to instruct the database to make changes. By contrast, the state-based approach appears to be a blackbox, and sometimes this may lead to an unexpected outcome. Let's give an example:
We have 2 engineers, Alice and Bob. Alice first made a schema change by adding columnA to a table. Meanwhile, Bob uses the schema version before Alice's change, adds columnB, and checks in after Alice's change.
With the state-based approach, Bob's check-in would cause the system to overwrite Alice's work, since the desired state from Bob's check-in does not include Alice's change. But with the migration-based approach, because of its incremental nature, it won't cause a problem.
State-based version control requires more engineering discipline and tooling support to prevent conflicting changes, while migration-based version control is more forgivable to such conflict.
The stakes are high when dealing with data, and people tend to prefer the method providing better control and a safety fallback at the cost of usability.
How Bytebase could help
State-based approach took Infrastructure as Code by storm with the help of Kubernetes, HashiCorp Terraform and the like.
But for database version control, we believe the right approach is still migration-based. This is partly due to the lack of engine support in MySQL, PostgreSQL, partly due to the complexity of managing data (application state), as well as the high cost of making mistakes.
At the same time, we also understand the benefit of the state-based approach. That's why back in version 0.5.0, Bytebase introduced the schema snapshot and write-back feature:
- For every schema migration, Bytebase records the schema snapshot.
- If a team manages database schema under a version control system, they can configure Bytebase to write the schema snapshot back to the repository at a specified path.
Five years on, that hybrid model is more built out. Bytebase writes the cumulative schema back after each migration, so the repo carries one up-to-date schema file (the SSOT) right next to the migration history. Teams retain all the existing benefits of the migration-based approach, and they also get the single-source-of-truth merit from the state-based approach. Do check out the user guide on how to configure this.
We have also gone a step further: Bytebase now supports true state-based schema change, where you declare the desired schema and Bytebase computes and applies the diff for you. For now this is Postgres only. As discussed above, the reconciliation is the hard part, and we would rather get it right one engine at a time than ship something half-working everywhere. Migration-based stays the default, and remains the only option on the other engines for now.
As a side note, I do still hope database engines could innovate in the direction Kubernetes did, to pave the way for the state-based approach. Atlas and SchemaHero have made it credible at the tooling layer, but the engines themselves haven't moved. Google's own database system Spanner does provide this support, and you know what, every team at Google uses the state-based approach to manage their database schemas.
The whole industry is closer than it was in 2021, but the engines still aren't there. Until they are, hybrid is the pragmatic answer, and at least Bytebase can bridge the gap. For Postgres, you can already go fully state-based today.