Cross-module migration solution design

Changes list



Raman AuramauAdded sequence diagram and steps-to-be-followed


Raman AuramauAdded more details after a talk with Yauheni Kuzmianok


Raman AuramauInitial document


This is a solution design document aimed to provide details, alternatives and decision for FOLIO cross-module access pattern.

Refer to MODORDSTOR-213 - Getting issue details... STATUS and MODFIN-192 - Getting issue details... STATUS for tracking.

Problem Statement

Let's consider the following scenario: mod-orders-storage module needs to be populated with some data from mod_finance_storage module. A quick workaround implemented in migration script - - directly accesses mod_invoice_storage database tables.

This violates the module separation approach since the FOLIO architecture mandates that cross-module communication goes through HTTP APIs only. Also in some cases this workaround may fail running of migration script because some deployments use database ROLEs with access restricted to the current schema, so that mod-finance-storage doesn't have any permissions for mod_invoice_storage tables.

Options considered

Currently there is no approach in FOLIO to address the described problem. The general idea is to move the cross-module migration code into some back-end module. Depending on this code location, one can consider these options as possible approaches:

  1. Implementing generic approach for the whole FOLIO platform
    1. this use case can be covered on RMB level though this is a zone of responsibility of a team rather than Thunderjet,
    2. not sure if there are any real use cases known at the moment; in theory, there might be really different and much more complex use cases,
    3. so, this option has no continuation now.
  2. Designing a generic pattern and implementing it locally in the mod-finance-storage
    1. this option can be covered by Thunderjet team by its own without impact or dependencies on others modules / teams,
    2. this implementation - after review and release - can be considered as a pattern in case of similar needs.

Implementing a cross-module access approach in the mod-finance-storage

In general, this option is based on handling data retrieving in Java code. E.g., in mod-finance-storage is to be extended - when upgrading mod-finance-storage

  • query the database to discover which orders you need additional info about,
  • then make API calls to mod-orders-storage (note -storage, to avoid circular dependency) as needed to retrieve the workflow status, etc. of those orders,
  • finally, update the records in mod-finance's storage directly via PostgresClient.

Note that migration process is applicable to *-storage modules only, so that migration logic cannot be implemented on upper level of business modules. Another benefit in this context is that all the migration is executed in the same database transaction which guarantees data consistency after migration completion.

Described flow is also visualized in the following sequence diagram:

Limitations and Assumptions

  • Only Read operations from an another module storage can be covered with API.
  • A new dependency (as optional one) from mod-finance-storage onto mod-orders-storage is to be added for API calls enabling. If there will be a need for migration in another direction - from finance to orders - then circular dependency most likely will appear.
  • A clean architecture uses a business logic module (mod-orders) to control the migration. When the business logic module is upgraded it reads the data from mod-finance-storage and passes them to mod-orders-storage. A storage module should not depend on another storage module. This architecture avoids this dependency, and also avoids any circular dependency.
  • Clear installation from scratch should not invoke migration scripts.

Steps to apply the implemented logic

The skeleton of this approach has been already created and available via

The following steps are to be followed:

  • Find your TenantReferenceAPI class that extends TenantAPI
  • Copy next methods with migration execution logic
private Future<Void> migration(TenantAttributes attributes, String migrationModule, Supplier<Future<Void>> supplier) {
	SemVer moduleTo = moduleVersionToSemVer(migrationModule);
	SemVer currentModuleVersion = moduleVersionToSemVer(attributes.getModuleFrom());
	if (moduleTo.compareTo(currentModuleVersion) > 0){
		return supplier.get();
	return Future.succeededFuture();

private static SemVer moduleVersionToSemVer(String version) {
	try {
		return new SemVer(version);
	} catch (IllegalArgumentException ex) {
		return new ModuleId(version).getSemVer();
  • Extend loadData method with migration logic like this. Note that it's possible to explicitly specify versions which are to be checked, and to execute appropriate migration logic
 return Future.succeededFuture()
	// migrationModule value it the same as fromModuleVersion from schema.json
	.compose(v -> migration(attributes, "mod-finance-storage-6.1.0", () -> customizeYourMigrationLogicHere(headers, vertxContext)))
	.compose(v -> migration(attributes, "mod-finance-storage-5.0.0", () -> customizeYourMigrationLogicHere(headers, vertxContext)))
	.compose(v -> Future.future(promise -> tl.perform(attributes, headers, vertx, promise)));
  • Implement your own customizeYourMigrationLogicHere method with migration logic.