This is a length answer with two approach that work.
- a convention based versioning with out of order execution
- multiple schemas
Convention Based Approach
One way that this can be done is to follow a convention of assigning each module a version number and then have all the modules migrations be minor versions of the major version. For example assume that you have the following module structure.
config
module containing common spring boot configs all other modules inherit from this module. This is module where application.yml will be kept
user
module containing the user registration module, user
depends on config
email
module containing code for sending emails in the background, email
depends on config
The user module will have a db/migration
folder with the following files in it.
V2.001__create_users_schema.sql
V2.002__create_account_tables.sql
V2.003__create_x_tables.sql
The email
module will have a db/migration
folder with the following files in it
V3.001__create_email_schema.sql
V3.002__create_outbox_table.sql
With the above versioning convention you can always go back and add a new module specific migration. For example after the above migrations are applied you can add a V2.004__create_y_table.sql
and flyway will fill in the migration between V2.003
and V3.0001
You will need to configure flyway to allow out order migrations otherwise you will get an error. in boot you can set.
spring:
flyway:
out-of-order: true
The key thing is that the first migration file for each module start by issuing a CREATE SCHEMA
statement then follow up migrations include the schema name in all DDL statements or object references.
For example V2.001__create_users_schema.sql
contains
CREATE SCHEMA users;
and V2.002__create_account_tables.sql
contains
CREATE TABLE users.login(
username text
);
Notice that the name passed to the CREATE TABLE
is users.login
which includes the schema name.
By using separate schemas for each module it will be easier to extract modules and their db schemas into separate binaries in the future. Since spring boot is using a single database connection pool you must have 1 database user that has access to the schemas of all the modules. This requires vigilance to make sure the following things don't happen accidentally.
- write a view, query, stored procedure that references multiple schemas
- have one module insert data into another module's schema
- have
@Transactional
methods in different modules call each other. This one is the easiest to mess up since modules will typically call each other.You can try to work around this issue in two ways. Option 1 All @Service classes are package protected, modules call each other over HTTP only. Option 2 All @Service class methods use requires new trascation propagation @Transactional(propagation = Propagation.REQUIRES_NEW)
. I think option 1 is better if the goal is to eventually extract to a microservices architecture.
This approach works with PostgreSQL not sure how well it works with other databases and is based on the the FAQ https://flywaydb.org/documentation/faq#hot-fixes
Multiple Flyway Instances
SpringBoot looks for a bean of type FlywayMigrationStrategy
when its time to perform the migration. You can implement this interface ignore the app level migration and create several module specific migrations, code below works.
import org.flywaydb.core.Flyway;
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
import org.springframework.stereotype.Component;
@Component
public class MultiModuleFlywayMigrationStrategy implements FlywayMigrationStrategy {
@Override
public void migrate(Flyway flyway) {
var dataSource = flyway.getConfiguration().getDataSource();
Flyway testModule = Flyway.configure()
.schemas("test")
.locations("db/test")
.dataSource(dataSource).load();
Flyway ratingsModule = Flyway.configure()
.schemas("rating")
.locations("db/ratings")
.dataSource(dataSource).load();
// don't call flyway.migrate() since we don't want any migrations in db/migration
testModule.migrate();
ratingsModule.migrate();
}
}