Keeping the database state in sync with the code evolution is a natural process in software development and should be straightforward most of the time. But, during my career, I have encountered that some teams are having real struggles putting this into the right workflow without creating additional overhead. Except for doing manual migrations by executing SQL scripts directly on the database every time before deploying a new version of code, there are also tools that provide automated way of syncing code with the database.

I have put here the most important questions which I think we need to ask ourselves when it comes to automatic migration tools:

  • Which tool can effortlessly provide auto migrations and rollback?
  • How does it behave when more people are working on the same codebase/db models?
  • How does it behave when there are multiple app instances hitting the same database?

In the .NET world in general, we have the two most popular libraries: EF Migrations and Fluent Migrator competing with each other in different segments. Let us try to answer the above questions through some comparison between the two.

Auto Migrations and Rollback

  EF Migrations FluentMigrator
Migration creation Can be generated from model changes Explicitly defined
Auto-apply migrations on startup Supported Supported
Rollback Supported Supported
Control over migration More implicit/ORM-driven More explicit
SQL control Moderate High

We see that both libraries provide the same features, but in different ways. EF Migrations are model-driven and tightly integrated with rest of the .NET framework, while FluentMigrator is more lightweight when it comes to dependencies and gives more control over the code because migrations are explicitly defined in code.

More Branches vs Same Database

Here, I would give more points to FluentMigrator, since it offers greater flexibility compared to standard EF migrations, which are closely tied to EF entities and generated automatically, often accompanied by additional overhead, including extra snapshot files.

Things with snapshot files will become messy once there are more people working on db migrations at the same time. Imagine two of them creating two different branches out of one base snapshot, and then trying to update the db model independently on two different sides. When both are merged into the main branch, the migration history and snapshot can get out of sync, and the second migration may require additional changes or reconciliation before it can be applied successfully.

On the other hand, FluentMigrator does not have such issues, because it does not have checks on model and db snapshots. It just executes the code which is needed for the db to get updated to the new version. And this is, in my opinion, one really big time saver in the first place and it also reduces the possibility of unintentional code breaks during the migration phase.

Multiple Application Instances vs Same Database

Here, things can quickly become very messy. When dealing with a real production environment, the setup mostly consists of multiple app instances targeting the same database, and migrations can become challenging. Imagine the following scenario:

DatabaseOld Instance CNew Instance BNew Instance ADatabaseOld Instance CNew Instance BNew Instance ABoth instances try to  apply the migrationDatabase schema has changedMigrate UpMigrate UpMigration succeedsMigration already appliedQuery using old schemaQuery fails

We have three app instances and they are all running migration up at startup. We have published a new code version, and we want all our apps to update accordingly. In real life, it can happen that one of the app instances has not yet been updated while the database behind it has already been updated. As a result,old queries might fail against the new database setup.

It can also happen that two app instances are trying to update the database at the same time. Which migration will succeed and which one will fail? Do we have a locking mechanism inside the libraries? Libraries do provide some sort of locking mechanism, but still we need to rethink how the deployment architecture should be designed if we want to achieve seamless updates every time.

Let us take a look at the diagram below:

DatabaseOld Instance COld Instance BNew Instance ALoad BalancerDatabaseOld Instance COld Instance BNew Instance ALoad BalancerOld version is serving trafficA is ready but B and C are still servingOnly new version is serving trafficAll instances are up and running  Sending traffic to all of themDeploy new versionKeep A out of trafficAcquire migration lockMigrate UpMigration completedRelease migration lockRun health checksStart sending trafficStop new requestsStop new requestsDrain existing requestsDrain existing requestsDeploy new versionDeploy new version

This architecture is imagined in such a way that first app instance ready for the update takes a migration lock and performs the update, while the other instances keep serving the old version in the meantime. A migration lock prevents multiple instances from running migrations at the same time. It does not prevent old application instances from sending queries that are incompatible with the new schema.

Once the migration is done, the instance that performed the migration will be the only one serving the traffic, while others will be temporarily removed from the load balancer and updated. Once they are up and running again, the load balancer resumes its normal configuration and we have all apps up and running with almost zero downtime.

True zero downtime cannot be guaranteed simply by using migration locks, since it will always be a period when some requests might be invalid. It requires both safe database changes and a deployment strategy that can switch traffic without interrupting requests. Of course, if migration is large, this potential downtime increases, but that is one more reason to keep updates smaller.

In the end, all depends on your setup, and the problems you are trying to solve. Here, I am only giving the general idea and some framework for thinking when it comes to real problems.

Final Words

When it comes to choice of a tool/library, I am in favor of FluentMigrator as it provides more flexibility and is not coupled with the rest of EntityFramework. I have explained the syntax and how the library can be used in FluentMigrator tutorial: creating and configuring database migrations. In that same article, I also touched on the key differences between EF Migrations and FluentMigrator, so it can be used as a checklist if you are in doubt which one to use. On top of that, there is also a different article on how it can be set up for .NET Core Web API project, so it is worth checking it out.