You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2022/06/14 07:15:19 UTC

[fineract] 01/02: Fineract with ReadOnly tenant database connection

This is an automated email from the ASF dual-hosted git repository.

arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git

commit 2a7d8b94960d3dce0f85677f1faeec996c726df9
Author: Jose Alberto Hernandez <al...@MacBook-Pro.local>
AuthorDate: Tue May 31 07:22:54 2022 -0500

    Fineract with ReadOnly tenant database connection
---
 build.gradle                                       |  12 ++
 fineract-provider/build.gradle                     |   2 +
 .../domain/FineractPlatformTenantConnection.java   |  41 +++++-
 .../service/DataSourcePerTenantServiceFactory.java | 113 ++++++++++++++++
 .../TomcatJdbcDataSourcePerTenantService.java      |  44 +-----
 .../security/constants/TenantConstants.java        |  33 +++++
 .../service/BasicAuthTenantDetailsServiceJdbc.java |  90 -------------
 .../security/service/JdbcTenantDetailsService.java |  87 +-----------
 .../security/service/TenantMapper.java             | 124 +++++++++++++++++
 .../tenant-store/changelog-tenant-store.xml        |   1 +
 .../0004_readonly_database_connection.xml}         |  21 ++-
 .../tenant/parts/0014_remove_unused_jobs.xml       |   5 +
 .../TomcatJdbcDataSourcePerTenantServiceTest.java  | 147 +++++++++++++++++++++
 .../core/db/DatabaseConnectionProperties.java      | 111 ++++++++++++++++
 14 files changed, 615 insertions(+), 216 deletions(-)

diff --git a/build.gradle b/build.gradle
index c654d88df..3bba809a2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -539,6 +539,18 @@ configure(project.fineractJavaProjects) {
                 excludeTestsMatching project.property('excludeTests')
             }
         }
+
+        // Database type used in the Test
+        systemProperty 'dbType', 'mariadb'
+        if (project.hasProperty('dbType')) {
+            if ('postgresql'.equalsIgnoreCase(dbType)) {
+                systemProperty 'dbType', 'postgresql'
+            } else if ('mysql'.equalsIgnoreCase(dbType)) {
+                systemProperty 'dbType', 'mysql'
+            } else {
+                throw new GradleException('Provided dbType is not supported')
+            }
+        }
     }
 
     testlogger {
diff --git a/fineract-provider/build.gradle b/fineract-provider/build.gradle
index 2172b7a32..544f6ef23 100644
--- a/fineract-provider/build.gradle
+++ b/fineract-provider/build.gradle
@@ -223,6 +223,7 @@ bootRun {
     dependencies {
         implementation 'org.mariadb.jdbc:mariadb-java-client:2.7.5'
         implementation 'org.postgresql:postgresql:42.3.5'
+        implementation 'mysql:mysql-connector-java:8.0.29'
     }
 }
 
@@ -288,6 +289,7 @@ jib {
     dependencies {
         implementation 'org.mariadb.jdbc:mariadb-java-client:2.7.5'
         implementation 'org.postgresql:postgresql:42.3.5'
+        implementation 'mysql:mysql-connector-java:8.0.29'
     }
 
     pluginExtensions {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/FineractPlatformTenantConnection.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/FineractPlatformTenantConnection.java
index 293447a94..015da63cc 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/FineractPlatformTenantConnection.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/FineractPlatformTenantConnection.java
@@ -34,6 +34,12 @@ public class FineractPlatformTenantConnection {
     private final String schemaUsername;
     private final String schemaPassword;
     private final String schemaName;
+    private final String readOnlySchemaServer;
+    private final String readOnlySchemaServerPort;
+    private final String readOnlySchemaName;
+    private final String readOnlySchemaUsername;
+    private final String readOnlySchemaPassword;
+    private final String readOnlySchemaConnectionParameters;
     private final boolean autoUpdateEnabled;
     private final int initialSize;
     private final long validationInterval;
@@ -57,7 +63,9 @@ public class FineractPlatformTenantConnection {
             final boolean removeAbandoned, final int removeAbandonedTimeout, final boolean logAbandoned,
             final int abandonWhenPercentageFull, final int maxActive, final int minIdle, final int maxIdle, final int suspectTimeout,
             final int timeBetweenEvictionRunsMillis, final int minEvictableIdleTimeMillis, final int maxRetriesOnDeadlock,
-            final int maxIntervalBetweenRetries, final boolean tesOnBorrow) {
+            final int maxIntervalBetweenRetries, final boolean tesOnBorrow, final String readOnlySchemaServer,
+            final String readOnlySchemaServerPort, final String readOnlySchemaName, final String readOnlySchemaUsername,
+            final String readOnlySchemaPassword, final String readOnlySchemaConnectionParameters) {
 
         this.connectionId = connectionId;
         this.schemaName = schemaName;
@@ -82,6 +90,12 @@ public class FineractPlatformTenantConnection {
         this.maxRetriesOnDeadlock = maxRetriesOnDeadlock;
         this.maxIntervalBetweenRetries = maxIntervalBetweenRetries;
         this.testOnBorrow = tesOnBorrow;
+        this.readOnlySchemaServer = readOnlySchemaServer;
+        this.readOnlySchemaServerPort = readOnlySchemaServerPort;
+        this.readOnlySchemaName = readOnlySchemaName;
+        this.readOnlySchemaUsername = readOnlySchemaUsername;
+        this.readOnlySchemaPassword = readOnlySchemaPassword;
+        this.readOnlySchemaConnectionParameters = readOnlySchemaConnectionParameters;
     }
 
     public String getSchemaServer() {
@@ -176,6 +190,30 @@ public class FineractPlatformTenantConnection {
         return schemaName;
     }
 
+    public String getReadOnlySchemaServer() {
+        return readOnlySchemaServer;
+    }
+
+    public String getReadOnlySchemaServerPort() {
+        return readOnlySchemaServerPort;
+    }
+
+    public String getReadOnlySchemaName() {
+        return readOnlySchemaName;
+    }
+
+    public String getReadOnlySchemaUsername() {
+        return readOnlySchemaUsername;
+    }
+
+    public String getReadOnlySchemaPassword() {
+        return readOnlySchemaPassword;
+    }
+
+    public String getReadOnlySchemaConnectionParameters() {
+        return readOnlySchemaConnectionParameters;
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder(this.schemaName).append(":").append(this.schemaServer).append(":")
@@ -204,4 +242,5 @@ public class FineractPlatformTenantConnection {
             throw new RuntimeException(e);
         }
     }
+
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/DataSourcePerTenantServiceFactory.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/DataSourcePerTenantServiceFactory.java
new file mode 100644
index 000000000..676ec48a6
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/DataSourcePerTenantServiceFactory.java
@@ -0,0 +1,113 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.core.service;
+
+import static org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection.toJdbcUrl;
+import static org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection.toProtocol;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import javax.sql.DataSource;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
+import org.apache.fineract.infrastructure.security.constants.TenantConstants;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Component;
+
+/**
+ *
+ * Factory class to get data source service based on the details stored in {@link FineractPlatformTenantConnection}
+ * variable
+ *
+ */
+@Component
+@Slf4j
+@RequiredArgsConstructor
+public class DataSourcePerTenantServiceFactory {
+
+    private final HikariConfig hikariConfig;
+    private final FineractProperties fineractProperties;
+    private final ApplicationContext context;
+
+    public DataSource createNewDataSourceFor(final DataSource tenantDataSource, final FineractPlatformTenantConnection tenantConnection) {
+        String protocol = toProtocol(tenantDataSource);
+        // Default properties for Writing
+        String schemaServer = tenantConnection.getSchemaServer();
+        String schemaPort = tenantConnection.getSchemaServerPort();
+        String schemaName = tenantConnection.getSchemaName();
+        String schemaUsername = tenantConnection.getSchemaUsername();
+        String schemaPassword = tenantConnection.getSchemaPassword();
+        String schemaConnectionParameters = tenantConnection.getSchemaConnectionParameters();
+        // Properties to ReadOnly case
+        if (this.fineractProperties.getMode().isReadOnlyMode()) {
+            schemaServer = getPropertyValue(tenantConnection.getReadOnlySchemaServer(), TenantConstants.PROPERTY_RO_SCHEMA_SERVER_NAME,
+                    schemaServer);
+            schemaPort = getPropertyValue(tenantConnection.getReadOnlySchemaServerPort(), TenantConstants.PROPERTY_RO_SCHEMA_SERVER_PORT,
+                    schemaPort);
+            schemaName = getPropertyValue(tenantConnection.getReadOnlySchemaName(), TenantConstants.PROPERTY_RO_SCHEMA_SCHEMA_NAME,
+                    schemaName);
+            schemaUsername = getPropertyValue(tenantConnection.getReadOnlySchemaUsername(), TenantConstants.PROPERTY_RO_SCHEMA_USERNAME,
+                    schemaUsername);
+            schemaPassword = getPropertyValue(tenantConnection.getReadOnlySchemaPassword(), TenantConstants.PROPERTY_RO_SCHEMA_PASSWORD,
+                    schemaPassword);
+            schemaConnectionParameters = getPropertyValue(tenantConnection.getReadOnlySchemaConnectionParameters(),
+                    TenantConstants.PROPERTY_RO_SCHEMA_CONNECTION_PARAMETERS, schemaConnectionParameters);
+        }
+        String jdbcUrl = toJdbcUrl(protocol, schemaServer, schemaPort, schemaName, schemaConnectionParameters);
+        log.debug("{}", jdbcUrl);
+
+        HikariConfig config = new HikariConfig();
+        config.setReadOnly(this.fineractProperties.getMode().isReadOnlyMode());
+        config.setDriverClassName(hikariConfig.getDriverClassName());
+        config.setPoolName(schemaName + "_pool");
+        config.setJdbcUrl(jdbcUrl);
+        config.setUsername(schemaUsername);
+        config.setPassword(schemaPassword);
+        config.setMinimumIdle(tenantConnection.getInitialSize());
+        config.setMaximumPoolSize(tenantConnection.getMaxActive());
+        config.setConnectionTestQuery(hikariConfig.getConnectionTestQuery());
+        config.setValidationTimeout(tenantConnection.getValidationInterval());
+        config.setAutoCommit(hikariConfig.isAutoCommit());
+
+        // https://github.com/brettwooldridge/HikariCP/wiki/MBean-(JMX)-Monitoring-and-Management
+        config.setRegisterMbeans(true);
+
+        // https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
+        // These are the properties for each Tenant DB; the same configuration
+        // is also in src/main/resources/META-INF/spring/hikariDataSource.xml
+        // for the all Tenants DB -->
+        config.setDataSourceProperties(hikariConfig.getDataSourceProperties());
+
+        return new HikariDataSource(config);
+    }
+
+    private String getPropertyValue(final String baseValue, final String propertyName, final String defaultValue) {
+        // If the property already has set, return It
+        if (null != baseValue) {
+            return baseValue;
+        }
+        if (this.context == null) {
+            return defaultValue;
+        }
+        return this.context.getEnvironment().getProperty(propertyName, defaultValue);
+    }
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/TomcatJdbcDataSourcePerTenantService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/TomcatJdbcDataSourcePerTenantService.java
index 8be95f24e..26a708d2f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/TomcatJdbcDataSourcePerTenantService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/TomcatJdbcDataSourcePerTenantService.java
@@ -18,11 +18,6 @@
  */
 package org.apache.fineract.infrastructure.core.service;
 
-import static org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection.toJdbcUrl;
-import static org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection.toProtocol;
-
-import com.zaxxer.hikari.HikariConfig;
-import com.zaxxer.hikari.HikariDataSource;
 import java.util.HashMap;
 import java.util.Map;
 import javax.sql.DataSource;
@@ -44,12 +39,13 @@ public class TomcatJdbcDataSourcePerTenantService implements RoutingDataSourceSe
     private final Map<Long, DataSource> tenantToDataSourceMap = new HashMap<>(1);
     private final DataSource tenantDataSource;
 
-    @Autowired
-    private HikariConfig hikariConfig;
+    private final DataSourcePerTenantServiceFactory dataSourcePerTenantServiceFactory;
 
     @Autowired
-    public TomcatJdbcDataSourcePerTenantService(final @Qualifier("hikariTenantDataSource") DataSource tenantDataSource) {
+    public TomcatJdbcDataSourcePerTenantService(final @Qualifier("hikariTenantDataSource") DataSource tenantDataSource,
+            final DataSourcePerTenantServiceFactory dataSourcePerTenantServiceFactory) {
         this.tenantDataSource = tenantDataSource;
+        this.dataSourcePerTenantServiceFactory = dataSourcePerTenantServiceFactory;
     }
 
     @Override
@@ -68,7 +64,7 @@ public class TomcatJdbcDataSourcePerTenantService implements RoutingDataSourceSe
                 if (possibleDS != null) {
                     tenantDataSource = possibleDS;
                 } else {
-                    tenantDataSource = createNewDataSourceFor(tenantConnection);
+                    tenantDataSource = dataSourcePerTenantServiceFactory.createNewDataSourceFor(this.tenantDataSource, tenantConnection);
                     this.tenantToDataSourceMap.put(tenantConnection.getConnectionId(), tenantDataSource);
                 }
             }
@@ -76,34 +72,4 @@ public class TomcatJdbcDataSourcePerTenantService implements RoutingDataSourceSe
 
         return tenantDataSource;
     }
-
-    // creates the tenant data source for the oltp and report database
-    private DataSource createNewDataSourceFor(final FineractPlatformTenantConnection tenantConnectionObj) {
-        String protocol = toProtocol(this.tenantDataSource);
-        String jdbcUrl = toJdbcUrl(protocol, tenantConnectionObj.getSchemaServer(), tenantConnectionObj.getSchemaServerPort(),
-                tenantConnectionObj.getSchemaName(), tenantConnectionObj.getSchemaConnectionParameters());
-
-        HikariConfig config = new HikariConfig();
-        config.setDriverClassName(hikariConfig.getDriverClassName());
-        config.setPoolName(tenantConnectionObj.getSchemaName() + "_pool");
-        config.setJdbcUrl(jdbcUrl);
-        config.setUsername(tenantConnectionObj.getSchemaUsername());
-        config.setPassword(tenantConnectionObj.getSchemaPassword());
-        config.setMinimumIdle(tenantConnectionObj.getInitialSize());
-        config.setMaximumPoolSize(tenantConnectionObj.getMaxActive());
-        config.setConnectionTestQuery(hikariConfig.getConnectionTestQuery());
-        config.setValidationTimeout(tenantConnectionObj.getValidationInterval());
-        config.setAutoCommit(hikariConfig.isAutoCommit());
-
-        // https://github.com/brettwooldridge/HikariCP/wiki/MBean-(JMX)-Monitoring-and-Management
-        config.setRegisterMbeans(true);
-
-        // https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
-        // These are the properties for each Tenant DB; the same configuration
-        // is also in src/main/resources/META-INF/spring/hikariDataSource.xml
-        // for the all Tenants DB -->
-        config.setDataSourceProperties(hikariConfig.getDataSourceProperties());
-
-        return new HikariDataSource(config);
-    }
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TenantConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TenantConstants.java
new file mode 100644
index 000000000..551589989
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/constants/TenantConstants.java
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.security.constants;
+
+public final class TenantConstants {
+
+    private TenantConstants() {
+
+    }
+
+    public static final String PROPERTY_RO_SCHEMA_SERVER_NAME = "FINERACT_RO_SCHEMA_SERVER_NAME";
+    public static final String PROPERTY_RO_SCHEMA_SERVER_PORT = "FINERACT_RO_SCHEMA_SERVER_PORT";
+    public static final String PROPERTY_RO_SCHEMA_SCHEMA_NAME = "FINERACT_RO_SCHEMA_SCHEMA_NAME";
+    public static final String PROPERTY_RO_SCHEMA_USERNAME = "FINERACT_RO_SCHEMA_USERNAME";
+    public static final String PROPERTY_RO_SCHEMA_PASSWORD = "FINERACT_RO_SCHEMA_PASSWORD";
+    public static final String PROPERTY_RO_SCHEMA_CONNECTION_PARAMETERS = "FINERACT_RO_SCHEMA_CONNECTION_PARAMETERS";
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/BasicAuthTenantDetailsServiceJdbc.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/BasicAuthTenantDetailsServiceJdbc.java
index d5ec93483..a47556a28 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/BasicAuthTenantDetailsServiceJdbc.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/BasicAuthTenantDetailsServiceJdbc.java
@@ -18,18 +18,14 @@
  */
 package org.apache.fineract.infrastructure.security.service;
 
-import java.sql.ResultSet;
-import java.sql.SQLException;
 import javax.sql.DataSource;
 import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
-import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
 import org.apache.fineract.infrastructure.security.exception.InvalidTenantIdentiferException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.cache.annotation.Cacheable;
 import org.springframework.dao.EmptyResultDataAccessException;
 import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.jdbc.core.RowMapper;
 import org.springframework.stereotype.Service;
 
 /**
@@ -46,92 +42,6 @@ public class BasicAuthTenantDetailsServiceJdbc implements BasicAuthTenantDetails
         this.jdbcTemplate = new JdbcTemplate(dataSource);
     }
 
-    private static final class TenantMapper implements RowMapper<FineractPlatformTenant> {
-
-        private final boolean isReport;
-        private final StringBuilder sqlBuilder = new StringBuilder(" t.id, ts.id as connectionId , ")//
-                .append(" t.timezone_id as timezoneId , t.name,t.identifier, ts.schema_name as schemaName, ts.schema_server as schemaServer,")//
-                .append(" ts.schema_server_port as schemaServerPort, ts.schema_connection_parameters as schemaConnectionParameters, ts.auto_update as autoUpdate,")//
-                .append(" ts.schema_username as schemaUsername, ts.schema_password as schemaPassword , ts.pool_initial_size as initialSize,")//
-                .append(" ts.pool_validation_interval as validationInterval, ts.pool_remove_abandoned as removeAbandoned, ts.pool_remove_abandoned_timeout as removeAbandonedTimeout,")//
-                .append(" ts.pool_log_abandoned as logAbandoned, ts.pool_abandon_when_percentage_full as abandonedWhenPercentageFull, ts.pool_test_on_borrow as testOnBorrow,")//
-                .append(" ts.pool_max_active as poolMaxActive, ts.pool_min_idle as poolMinIdle, ts.pool_max_idle as poolMaxIdle,")//
-                .append(" ts.pool_suspect_timeout as poolSuspectTimeout, ts.pool_time_between_eviction_runs_millis as poolTimeBetweenEvictionRunsMillis,")//
-                .append(" ts.pool_min_evictable_idle_time_millis as poolMinEvictableIdleTimeMillis,")//
-                .append(" ts.deadlock_max_retries as maxRetriesOnDeadlock,")//
-                .append(" ts.deadlock_max_retry_interval as maxIntervalBetweenRetries ")//
-                .append(" from tenants t left join tenant_server_connections ts ");
-
-        TenantMapper(boolean isReport) {
-            this.isReport = isReport;
-        }
-
-        public String schema() {
-            if (this.isReport) {
-                this.sqlBuilder.append(" on t.report_Id = ts.id");
-            } else {
-                this.sqlBuilder.append(" on t.oltp_Id = ts.id");
-            }
-            return this.sqlBuilder.toString();
-        }
-
-        @Override
-        public FineractPlatformTenant mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
-            final Long id = rs.getLong("id");
-            final String tenantIdentifier = rs.getString("identifier");
-            final String name = rs.getString("name");
-            final String timezoneId = rs.getString("timezoneId");
-            final FineractPlatformTenantConnection connection = getDBConnection(rs);
-            return new FineractPlatformTenant(id, tenantIdentifier, name, timezoneId, connection);
-        }
-
-        // gets the DB connection
-        private FineractPlatformTenantConnection getDBConnection(ResultSet rs) throws SQLException {
-
-            final Long connectionId = rs.getLong("connectionId");
-            final String schemaName = rs.getString("schemaName");
-            final String schemaServer = rs.getString("schemaServer");
-            final String schemaServerPort = rs.getString("schemaServerPort");
-            final String schemaConnectionParameters = rs.getString("schemaConnectionParameters");
-            final String schemaUsername = rs.getString("schemaUsername");
-            final String schemaPassword = rs.getString("schemaPassword");
-            final boolean autoUpdateEnabled = rs.getBoolean("autoUpdate");
-            final int initialSize = rs.getInt("initialSize");
-            final boolean testOnBorrow = rs.getBoolean("testOnBorrow");
-            final long validationInterval = rs.getLong("validationInterval");
-            final boolean removeAbandoned = rs.getBoolean("removeAbandoned");
-            final int removeAbandonedTimeout = rs.getInt("removeAbandonedTimeout");
-            final boolean logAbandoned = rs.getBoolean("logAbandoned");
-            final int abandonWhenPercentageFull = rs.getInt("abandonedWhenPercentageFull");
-            final int maxActive = rs.getInt("poolMaxActive");
-            final int minIdle = rs.getInt("poolMinIdle");
-            final int maxIdle = rs.getInt("poolMaxIdle");
-            final int suspectTimeout = rs.getInt("poolSuspectTimeout");
-            final int timeBetweenEvictionRunsMillis = rs.getInt("poolTimeBetweenEvictionRunsMillis");
-            final int minEvictableIdleTimeMillis = rs.getInt("poolMinEvictableIdleTimeMillis");
-            int maxRetriesOnDeadlock = rs.getInt("maxRetriesOnDeadlock");
-            int maxIntervalBetweenRetries = rs.getInt("maxIntervalBetweenRetries");
-
-            maxRetriesOnDeadlock = bindValueInMinMaxRange(maxRetriesOnDeadlock, 0, 15);
-            maxIntervalBetweenRetries = bindValueInMinMaxRange(maxIntervalBetweenRetries, 1, 15);
-
-            return new FineractPlatformTenantConnection(connectionId, schemaName, schemaServer, schemaServerPort,
-                    schemaConnectionParameters, schemaUsername, schemaPassword, autoUpdateEnabled, initialSize, validationInterval,
-                    removeAbandoned, removeAbandonedTimeout, logAbandoned, abandonWhenPercentageFull, maxActive, minIdle, maxIdle,
-                    suspectTimeout, timeBetweenEvictionRunsMillis, minEvictableIdleTimeMillis, maxRetriesOnDeadlock,
-                    maxIntervalBetweenRetries, testOnBorrow);
-        }
-
-        private int bindValueInMinMaxRange(final int value, int min, int max) {
-            if (value < min) {
-                return min;
-            } else if (value > max) {
-                return max;
-            }
-            return value;
-        }
-    }
-
     @Override
     @Cacheable(value = "tenantsById")
     public FineractPlatformTenant loadTenantById(final String tenantIdentifier, final boolean isReport) {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/JdbcTenantDetailsService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/JdbcTenantDetailsService.java
index cf6e8d31f..4cbc62370 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/JdbcTenantDetailsService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/JdbcTenantDetailsService.java
@@ -18,19 +18,15 @@
  */
 package org.apache.fineract.infrastructure.security.service;
 
-import java.sql.ResultSet;
-import java.sql.SQLException;
 import java.util.List;
 import javax.sql.DataSource;
 import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
-import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
 import org.apache.fineract.infrastructure.security.exception.InvalidTenantIdentiferException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.cache.annotation.Cacheable;
 import org.springframework.dao.EmptyResultDataAccessException;
 import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.jdbc.core.RowMapper;
 import org.springframework.stereotype.Service;
 
 /**
@@ -47,90 +43,11 @@ public class JdbcTenantDetailsService implements TenantDetailsService {
         this.jdbcTemplate = new JdbcTemplate(dataSource);
     }
 
-    private static final class TenantMapper implements RowMapper<FineractPlatformTenant> {
-
-        private final StringBuilder sqlBuilder = new StringBuilder("t.id, ts.id as connectionId , ")//
-                .append(" t.timezone_id as timezoneId , t.name,t.identifier, ts.schema_name as schemaName, ts.schema_server as schemaServer,")//
-                .append(" ts.schema_server_port as schemaServerPort, ts.schema_connection_parameters as schemaConnectionParameters, ts.auto_update as autoUpdate,")//
-                .append(" ts.schema_username as schemaUsername, ts.schema_password as schemaPassword , ts.pool_initial_size as initialSize,")//
-                .append(" ts.pool_validation_interval as validationInterval, ts.pool_remove_abandoned as removeAbandoned, ts.pool_remove_abandoned_timeout as removeAbandonedTimeout,")//
-                .append(" ts.pool_log_abandoned as logAbandoned, ts.pool_abandon_when_percentage_full as abandonedWhenPercentageFull, ts.pool_test_on_borrow as testOnBorrow,")//
-                .append(" ts.pool_max_active as poolMaxActive, ts.pool_min_idle as poolMinIdle, ts.pool_max_idle as poolMaxIdle,")//
-                .append(" ts.pool_suspect_timeout as poolSuspectTimeout, ts.pool_time_between_eviction_runs_millis as poolTimeBetweenEvictionRunsMillis,")//
-                .append(" ts.pool_min_evictable_idle_time_millis as poolMinEvictableIdleTimeMillis,")//
-                .append(" ts.deadlock_max_retries as maxRetriesOnDeadlock,")//
-                .append(" ts.deadlock_max_retry_interval as maxIntervalBetweenRetries ")//
-                .append(" from tenants t left join tenant_server_connections ts on t.oltp_Id=ts.id ");
-
-        public String schema() {
-            return this.sqlBuilder.toString();
-        }
-
-        @Override
-        public FineractPlatformTenant mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
-            final Long id = rs.getLong("id");
-            final String tenantIdentifier = rs.getString("identifier");
-            final String name = rs.getString("name");
-            final String timezoneId = rs.getString("timezoneId");
-            final FineractPlatformTenantConnection connection = getDBConnection(rs);
-
-            return new FineractPlatformTenant(id, tenantIdentifier, name, timezoneId, connection);
-        }
-
-        // gets the DB connection
-        private FineractPlatformTenantConnection getDBConnection(ResultSet rs) throws SQLException {
-
-            final Long connectionId = rs.getLong("connectionId");
-            final String schemaName = rs.getString("schemaName");
-            final String schemaServer = rs.getString("schemaServer");
-            final String schemaServerPort = rs.getString("schemaServerPort");
-            final String schemaConnectionParameters = rs.getString("schemaConnectionParameters");
-            final String schemaUsername = rs.getString("schemaUsername");
-            final String schemaPassword = rs.getString("schemaPassword");
-            final boolean autoUpdateEnabled = rs.getBoolean("autoUpdate");
-            final int initialSize = rs.getInt("initialSize");
-            final boolean testOnBorrow = rs.getBoolean("testOnBorrow");
-            final long validationInterval = rs.getLong("validationInterval");
-            final boolean removeAbandoned = rs.getBoolean("removeAbandoned");
-            final int removeAbandonedTimeout = rs.getInt("removeAbandonedTimeout");
-            final boolean logAbandoned = rs.getBoolean("logAbandoned");
-            final int abandonWhenPercentageFull = rs.getInt("abandonedWhenPercentageFull");
-            final int maxActive = rs.getInt("poolMaxActive");
-            final int minIdle = rs.getInt("poolMinIdle");
-            final int maxIdle = rs.getInt("poolMaxIdle");
-            final int suspectTimeout = rs.getInt("poolSuspectTimeout");
-            final int timeBetweenEvictionRunsMillis = rs.getInt("poolTimeBetweenEvictionRunsMillis");
-            final int minEvictableIdleTimeMillis = rs.getInt("poolMinEvictableIdleTimeMillis");
-            int maxRetriesOnDeadlock = rs.getInt("maxRetriesOnDeadlock");
-            int maxIntervalBetweenRetries = rs.getInt("maxIntervalBetweenRetries");
-
-            maxRetriesOnDeadlock = bindValueInMinMaxRange(maxRetriesOnDeadlock, 0, 15);
-            maxIntervalBetweenRetries = bindValueInMinMaxRange(maxIntervalBetweenRetries, 1, 15);
-
-            return new FineractPlatformTenantConnection(connectionId, schemaName, schemaServer, schemaServerPort,
-                    schemaConnectionParameters, schemaUsername, schemaPassword, autoUpdateEnabled, initialSize, validationInterval,
-                    removeAbandoned, removeAbandonedTimeout, logAbandoned, abandonWhenPercentageFull, maxActive, minIdle, maxIdle,
-                    suspectTimeout, timeBetweenEvictionRunsMillis, minEvictableIdleTimeMillis, maxRetriesOnDeadlock,
-                    maxIntervalBetweenRetries, testOnBorrow);
-
-        }
-
-        private int bindValueInMinMaxRange(final int value, int min, int max) {
-            if (value < min) {
-                return min;
-            } else if (value > max) {
-                return max;
-            }
-            return value;
-        }
-    }
-
     @Override
     @Cacheable(value = "tenantsById")
     public FineractPlatformTenant loadTenantById(final String tenantIdentifier) {
-
         try {
-            final TenantMapper rm = new TenantMapper();
+            final TenantMapper rm = new TenantMapper(false);
             final String sql = "select " + rm.schema() + " where t.identifier = ?";
 
             return this.jdbcTemplate.queryForObject(sql, rm, new Object[] { tenantIdentifier }); // NOSONAR
@@ -141,7 +58,7 @@ public class JdbcTenantDetailsService implements TenantDetailsService {
 
     @Override
     public List<FineractPlatformTenant> findAllTenants() {
-        final TenantMapper rm = new TenantMapper();
+        final TenantMapper rm = new TenantMapper(false);
         final String sql = "select  " + rm.schema();
 
         final List<FineractPlatformTenant> fineractPlatformTenants = this.jdbcTemplate.query(sql, rm); // NOSONAR
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TenantMapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TenantMapper.java
new file mode 100644
index 000000000..02ade1944
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TenantMapper.java
@@ -0,0 +1,124 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fineract.infrastructure.security.service;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
+import org.springframework.jdbc.core.RowMapper;
+
+public final class TenantMapper implements RowMapper<FineractPlatformTenant> {
+
+    private final boolean isReport;
+    private static final String TENANT_SERVER_CONNECTION_BUILDER = " t.id, ts.id as connectionId , "
+            + " t.timezone_id as timezoneId , t.name,t.identifier, ts.schema_name as schemaName, ts.schema_server as schemaServer,"
+            + " ts.schema_server_port as schemaServerPort, ts.schema_connection_parameters as schemaConnectionParameters, ts.auto_update as autoUpdate,"
+            + " ts.schema_username as schemaUsername, ts.schema_password as schemaPassword , ts.pool_initial_size as initialSize,"
+            + " ts.pool_validation_interval as validationInterval, ts.pool_remove_abandoned as removeAbandoned, ts.pool_remove_abandoned_timeout as removeAbandonedTimeout,"
+            + " ts.pool_log_abandoned as logAbandoned, ts.pool_abandon_when_percentage_full as abandonedWhenPercentageFull, ts.pool_test_on_borrow as testOnBorrow,"
+            + " ts.pool_max_active as poolMaxActive, ts.pool_min_idle as poolMinIdle, ts.pool_max_idle as poolMaxIdle,"
+            + " ts.pool_suspect_timeout as poolSuspectTimeout, ts.pool_time_between_eviction_runs_millis as poolTimeBetweenEvictionRunsMillis,"
+            + " ts.pool_min_evictable_idle_time_millis as poolMinEvictableIdleTimeMillis,"
+            + " ts.deadlock_max_retries as maxRetriesOnDeadlock," + " ts.deadlock_max_retry_interval as maxIntervalBetweenRetries,"
+            + " ts.readonly_schema_server as readOnlySchemaServer, " + " ts.readonly_schema_server_port as readOnlySchemaServerPort, "
+            + " ts.readonly_schema_name as readOnlySchemaName, " + " ts.readonly_schema_username as readOnlySchemaUsername, "
+            + " ts.readonly_schema_password as readOnlySchemaPassword, "
+            + " ts.readonly_schema_connection_parameters as readOnlySchemaConnectionParameters "
+            + " from tenants t left join tenant_server_connections ts ";
+    private final StringBuilder sqlBuilder = new StringBuilder(TENANT_SERVER_CONNECTION_BUILDER);
+
+    TenantMapper(boolean isReport) {
+        this.isReport = isReport;
+    }
+
+    public String schema() {
+        if (this.isReport) {
+            this.sqlBuilder.append(" on t.report_Id = ts.id");
+        } else {
+            this.sqlBuilder.append(" on t.oltp_Id = ts.id");
+        }
+        return this.sqlBuilder.toString();
+    }
+
+    @Override
+    public FineractPlatformTenant mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
+        final Long id = rs.getLong("id");
+        final String tenantIdentifier = rs.getString("identifier");
+        final String name = rs.getString("name");
+        final String timezoneId = rs.getString("timezoneId");
+        final FineractPlatformTenantConnection connection = getDBConnection(rs);
+        return new FineractPlatformTenant(id, tenantIdentifier, name, timezoneId, connection);
+    }
+
+    // gets the DB connection
+    private FineractPlatformTenantConnection getDBConnection(ResultSet rs) throws SQLException {
+
+        final Long connectionId = rs.getLong("connectionId");
+        final String schemaName = rs.getString("schemaName");
+        final String schemaServer = rs.getString("schemaServer");
+        final String schemaServerPort = rs.getString("schemaServerPort");
+        final String schemaConnectionParameters = rs.getString("schemaConnectionParameters");
+        final String schemaUsername = rs.getString("schemaUsername");
+        final String schemaPassword = rs.getString("schemaPassword");
+        final String readOnlySchemaName = rs.getString("readOnlySchemaName");
+        final String readOnlySchemaServer = rs.getString("readOnlySchemaServer");
+        final String readOnlySchemaServerPort = rs.getString("readOnlySchemaServerPort");
+        final String readOnlySchemaUsername = rs.getString("readOnlySchemaUsername");
+        final String readOnlySchemaPassword = rs.getString("readOnlySchemaPassword");
+        final String readOnlySchemaConnectionParameters = rs.getString("readOnlySchemaConnectionParameters");
+
+        final boolean autoUpdateEnabled = rs.getBoolean("autoUpdate");
+        final int initialSize = rs.getInt("initialSize");
+        final boolean testOnBorrow = rs.getBoolean("testOnBorrow");
+        final long validationInterval = rs.getLong("validationInterval");
+        final boolean removeAbandoned = rs.getBoolean("removeAbandoned");
+        final int removeAbandonedTimeout = rs.getInt("removeAbandonedTimeout");
+        final boolean logAbandoned = rs.getBoolean("logAbandoned");
+        final int abandonWhenPercentageFull = rs.getInt("abandonedWhenPercentageFull");
+        final int maxActive = rs.getInt("poolMaxActive");
+        final int minIdle = rs.getInt("poolMinIdle");
+        final int maxIdle = rs.getInt("poolMaxIdle");
+        final int suspectTimeout = rs.getInt("poolSuspectTimeout");
+        final int timeBetweenEvictionRunsMillis = rs.getInt("poolTimeBetweenEvictionRunsMillis");
+        final int minEvictableIdleTimeMillis = rs.getInt("poolMinEvictableIdleTimeMillis");
+        int maxRetriesOnDeadlock = rs.getInt("maxRetriesOnDeadlock");
+        int maxIntervalBetweenRetries = rs.getInt("maxIntervalBetweenRetries");
+
+        maxRetriesOnDeadlock = bindValueInMinMaxRange(maxRetriesOnDeadlock, 0, 15);
+        maxIntervalBetweenRetries = bindValueInMinMaxRange(maxIntervalBetweenRetries, 1, 15);
+
+        return new FineractPlatformTenantConnection(connectionId, schemaName, schemaServer, schemaServerPort, schemaConnectionParameters,
+                schemaUsername, schemaPassword, autoUpdateEnabled, initialSize, validationInterval, removeAbandoned, removeAbandonedTimeout,
+                logAbandoned, abandonWhenPercentageFull, maxActive, minIdle, maxIdle, suspectTimeout, timeBetweenEvictionRunsMillis,
+                minEvictableIdleTimeMillis, maxRetriesOnDeadlock, maxIntervalBetweenRetries, testOnBorrow, readOnlySchemaServer,
+                readOnlySchemaServerPort, readOnlySchemaName, readOnlySchemaUsername, readOnlySchemaPassword,
+                readOnlySchemaConnectionParameters);
+    }
+
+    private int bindValueInMinMaxRange(final int value, int min, int max) {
+        if (value < min) {
+            return min;
+        } else if (value > max) {
+            return max;
+        }
+        return value;
+    }
+}
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml b/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml
index ae142fd40..8c8440243 100644
--- a/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml
+++ b/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml
@@ -23,4 +23,5 @@
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
      <include file="parts/0003_reset_postgresql_sequences.xml" relativeToChangelogFile="true"/>
+     <include file="parts/0004_readonly_database_connection.xml" relativeToChangelogFile="true"/>
 </databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml b/fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0004_readonly_database_connection.xml
similarity index 52%
copy from fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml
copy to fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0004_readonly_database_connection.xml
index ae142fd40..4b465a8f0 100644
--- a/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml
+++ b/fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0004_readonly_database_connection.xml
@@ -22,5 +22,24 @@
 <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
-     <include file="parts/0003_reset_postgresql_sequences.xml" relativeToChangelogFile="true"/>
+    <changeSet author="fineract" id="add-readonly-database-connection-properties" context="tenant_store_db">
+        <addColumn tableName="tenant_server_connections">
+            <column name="readonly_schema_server" type="VARCHAR(100)"/>
+        </addColumn>
+        <addColumn tableName="tenant_server_connections">
+            <column name="readonly_schema_name" type="VARCHAR(100)"/>
+        </addColumn>
+        <addColumn tableName="tenant_server_connections">
+            <column name="readonly_schema_server_port" type="VARCHAR(10)"/>
+        </addColumn>
+        <addColumn tableName="tenant_server_connections">
+            <column name="readonly_schema_username" type="VARCHAR(100)"/>
+        </addColumn>
+        <addColumn tableName="tenant_server_connections">
+            <column name="readonly_schema_password" type="VARCHAR(100)"/>
+        </addColumn>
+        <addColumn tableName="tenant_server_connections">
+            <column name="readonly_schema_connection_parameters" type="TEXT"/>
+        </addColumn>
+    </changeSet>
 </databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0014_remove_unused_jobs.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0014_remove_unused_jobs.xml
index 8359f9d64..0f7d536ee 100644
--- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0014_remove_unused_jobs.xml
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0014_remove_unused_jobs.xml
@@ -25,6 +25,11 @@
         <dropForeignKeyConstraint baseTableName="job_run_history" constraintName="scheduledjobsFK"/>
     </changeSet>
     <changeSet author="fineract" id="1">
+        <validCheckSum>8:18ed72e1958f7db9c1a0c983ac823ab3</validCheckSum> <!-- checksum withsout delete from history -->
+        <validCheckSum>8:b4a83f9764270372a384616acf066d7c</validCheckSum> <!-- checksum with delete from history -->
+        <sql>
+            DELETE FROM job_run_history WHERE job_id IN (SELECT id FROM job WHERE name IN ('Update loan Summary', 'Update Loan Paid In Advance'))
+        </sql>
         <delete tableName="job">
             <where>name='Update loan Summary'</where>
         </delete>
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/TomcatJdbcDataSourcePerTenantServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/TomcatJdbcDataSourcePerTenantServiceTest.java
new file mode 100644
index 000000000..7257301fa
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/TomcatJdbcDataSourcePerTenantServiceTest.java
@@ -0,0 +1,147 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.core;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+import java.util.Properties;
+import javax.sql.DataSource;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.db.DatabaseConnectionProperties;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
+import org.apache.fineract.infrastructure.core.service.DataSourcePerTenantServiceFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.test.context.TestPropertySource;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+@TestPropertySource("classpath:application-test.properties")
+public class TomcatJdbcDataSourcePerTenantServiceTest {
+
+    @Mock
+    private FineractProperties fineractProperties;
+
+    @Mock
+    private HikariConfig hikariConfig;
+
+    @Mock
+    private Connection connection;
+
+    @Mock
+    private FineractPlatformTenant defaultTenant;
+
+    @Mock
+    private FineractPlatformTenantConnection tenantConnection;
+
+    @InjectMocks
+    private DataSourcePerTenantServiceFactory underTest;
+
+    private DataSource dataSource;
+    private DataSource tenantDataSource;
+    private DatabaseConnectionProperties databaseConnectionProperties;
+
+    @BeforeEach
+    void setUp() throws SQLException {
+        this.databaseConnectionProperties = DatabaseConnectionProperties.instance(System.getProperty("dbType"));
+
+        tenantDataSource = mock(HikariDataSource.class);
+        given(tenantDataSource.getConnection()).willReturn(connection);
+        given(connection.getMetaData()).willReturn(mock(DatabaseMetaData.class));
+        given(connection.getMetaData().getURL()).willReturn(databaseConnectionProperties.getJdbcUrl());
+
+        given(tenantConnection.getSchemaServer()).willReturn(databaseConnectionProperties.getSchemaServer());
+        given(tenantConnection.getSchemaServerPort()).willReturn(databaseConnectionProperties.getSchemaPort());
+        given(tenantConnection.getSchemaUsername()).willReturn(databaseConnectionProperties.getSchemaUsername());
+        given(tenantConnection.getSchemaPassword()).willReturn(databaseConnectionProperties.getSchemaPassword());
+        given(tenantConnection.getSchemaName()).willReturn(databaseConnectionProperties.getSchemaName());
+
+        given(defaultTenant.getConnection()).willReturn(tenantConnection);
+        given(defaultTenant.getConnection().getMaxActive()).willReturn(5);
+        given(defaultTenant.getConnection().getValidationInterval()).willReturn(500L);
+
+        given(hikariConfig.getInitializationFailTimeout()).willReturn(0L);
+        given(hikariConfig.getDriverClassName()).willReturn(databaseConnectionProperties.getDriverClassName());
+        given(hikariConfig.getConnectionTestQuery()).willReturn(databaseConnectionProperties.getConnectionTestQuery());
+        given(hikariConfig.getDataSourceProperties()).willReturn(mock(Properties.class));
+        given(hikariConfig.isAutoCommit()).willReturn(true);
+    }
+
+    @Test
+    void testRetrieveDataSource_ShouldCreateDataSource_WhenFineractIsInAllMode() {
+        // given
+        FineractProperties.FineractModeProperties modeProperties = createModeProps(true, true, true);
+        given(fineractProperties.getMode()).willReturn(modeProperties);
+
+        // when
+        dataSource = underTest.createNewDataSourceFor(tenantDataSource, defaultTenant.getConnection());
+
+        // then
+        assertNotNull(dataSource);
+    }
+
+    @Test
+    void testRetrieveDataSource_ShouldCreateDataSource_WhenFineractIsInReadOnlyMode() {
+        // given
+        FineractProperties.FineractModeProperties modeProperties = createModeProps(true, false, false);
+        given(fineractProperties.getMode()).willReturn(modeProperties);
+
+        // when
+        dataSource = underTest.createNewDataSourceFor(tenantDataSource, defaultTenant.getConnection());
+
+        // then
+        assertNotNull(dataSource);
+    }
+
+    @Test
+    void testRetrieveDataSource_ShouldCreateDataSource_WhenFineractIsInBatchMode() {
+        // given
+        FineractProperties.FineractModeProperties modeProperties = createModeProps(false, false, true);
+        given(fineractProperties.getMode()).willReturn(modeProperties);
+
+        // when
+        dataSource = underTest.createNewDataSourceFor(tenantDataSource, defaultTenant.getConnection());
+
+        // then
+        assertNotNull(dataSource);
+    }
+
+    private FineractProperties.FineractModeProperties createModeProps(boolean readEnabled, boolean writeEnabled, boolean batchEnabled) {
+        FineractProperties.FineractModeProperties modeProperties = new FineractProperties.FineractModeProperties();
+        modeProperties.setReadEnabled(readEnabled);
+        modeProperties.setWriteEnabled(writeEnabled);
+        modeProperties.setBatchEnabled(batchEnabled);
+        return modeProperties;
+    }
+
+}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/db/DatabaseConnectionProperties.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/db/DatabaseConnectionProperties.java
new file mode 100644
index 000000000..d962239ef
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/db/DatabaseConnectionProperties.java
@@ -0,0 +1,111 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fineract.infrastructure.core.db;
+
+public class DatabaseConnectionProperties {
+
+    private String driverClassName;
+    private String jdbcUrl;
+    private String connectionTestQuery;
+
+    private String schemaServer;
+    private String schemaName;
+    private String schemaPort;
+    private String schemaUsername;
+    private String schemaPassword;
+
+    protected DatabaseConnectionProperties(final String driverClassName, final String jdbcUrl, final String schemaPort,
+            final String schemaPassword) {
+        this.driverClassName = driverClassName;
+        this.jdbcUrl = jdbcUrl;
+        this.schemaPort = schemaPort;
+        this.schemaPassword = schemaPassword;
+
+        this.schemaServer = "localhost";
+        this.schemaUsername = "root";
+        this.schemaName = "fineract_tenants";
+        this.connectionTestQuery = "SELECT 1";
+    }
+
+    public static DatabaseConnectionProperties instance(final String dbType) {
+        return new DatabaseConnectionProperties(getDbDriverClassName(dbType), buildJdbcUrl(dbType), getDbPort(dbType),
+                getDbPassword(dbType));
+    }
+
+    private static String getDbDriverClassName(final String dbType) {
+        if (dbType.equals("mariadb")) {
+            return "org.mariadb.jdbc.Driver";
+        } else if (dbType.equals("mysql")) {
+            return "com.mysql.cj.jdbc.Driver";
+        } else {
+            return "org.postgresql.Driver";
+        }
+    }
+
+    private static String buildJdbcUrl(final String dbType) {
+        return "jdbc:" + dbType + "://localhost:" + getDbPort(dbType) + "/fineract_tenants";
+    }
+
+    private static String getDbPort(final String dbType) {
+        if (dbType.equals("postgresql")) {
+            return "5432";
+        }
+        return "3306";
+    }
+
+    private static String getDbPassword(final String dbType) {
+        if (dbType.equals("postgresql")) {
+            return "postgres";
+        }
+        return "mysql";
+    }
+
+    public String getDriverClassName() {
+        return driverClassName;
+    }
+
+    public String getJdbcUrl() {
+        return jdbcUrl;
+    }
+
+    public String getConnectionTestQuery() {
+        return connectionTestQuery;
+    }
+
+    public String getSchemaServer() {
+        return schemaServer;
+    }
+
+    public String getSchemaName() {
+        return schemaName;
+    }
+
+    public String getSchemaPort() {
+        return schemaPort;
+    }
+
+    public String getSchemaUsername() {
+        return schemaUsername;
+    }
+
+    public String getSchemaPassword() {
+        return schemaPassword;
+    }
+}