Modernising Oracle Stored Procedures to Java Services in Banking and Insurance (with GitLab Duo)

Blog
Modernising Oracle Stored Procedures to Java Services in Banking and Insurance (with GitLab Duo)
Banking and insurance systems often sit on top of battle-tested Oracle databases. Over the years, a huge amount of business logic has migrated into PL/SQL stored procedures: customer onboarding, KYC checks, fund transfers, premium calculations, claim adjudication… you name it.
This architecture has worked for decades. But it comes with a cost:
- Business rules are trapped in the database
- Changes are risky and slow
- Automated testing is hard
- Scaling is constrained by the database tier
Modernisation usually means moving this logic into Java services (e.g., Spring Boot), while retaining Oracle as a reliable system of record. In this journey, tools like GitLab Duo act as copilots: helping you understand legacy PL/SQL, sketch new service layers, and generate tests and pipelines.
In this blog, we’ll walk through:
- What a stored-procedure–centric Oracle application looks like in BFSI
- A target Java-based architecture
- A practical migration strategy
- A concrete example: turning an Oracle fund transfer stored procedure into a Java service
- How GitLab Duo helps along the way
- Before/after comparison and lessons learned
1. What “Stored-Procedure–Centric” Looks Like in BFSI
In many banks and insurers, the core systems resemble this:
Legacy Architecture:
[Channel (Web/Mobile/Branch)] → [App Server / ESB] → [Oracle DB] → [PL/SQL Packages & Procedures]
Examples from banking:
SP_OPEN_SAVINGS_ACCOUNTSP_TRANSFER_FUNDSPKG_LOAN_DISBURSAL
Examples from insurance:
PKG_POLICY_ISSUANCESP_CALCULATE_PREMIUMSP_PROCESS_CLAIM
Common traits:
- Business logic lives in PL/SQL: validations, calculations, eligibility checks, fee/interest computations.
- The Java (or .NET) app often becomes a thin wrapper around stored procedures.
- Integrations call PL/SQL via JDBC CallableStatements, passing IN/OUT parameters.
- Testing is mostly manual or done via end-to-end UI tests.
- Deployments are split: DB changes vs app changes, with tricky coordination.
Why modernise?
- Need for microservices and API-driven architectures (open banking, embedded insurance).
- Stronger automated testing and CI/CD.
- More flexible scaling, especially for read-heavy and compute-heavy workloads.
- Easier talent hiring (Java developers are easier to find than PL/SQL experts).
- Regulatory & audit: better traceability, decoupling, and observability.
2. Target Java Architecture for Banking & Insurance
A modernised architecture typically looks like:
Target Architecture:
[Channels (Mobile, Web, Partner APIs)]
→ [API Gateway]
→ [Java Microservices (Spring Boot)]
→ [Oracle / Other Data Stores]
Key characteristics:
- Business logic moves into Java services
- Banking: Customer Service, Account Service, Payments Service
- Insurance: Policy Service, Pricing Service, Claims Service
- Oracle remains as a transactional system of record
- Data access via JDBC or JPA (Hibernate), not via stored procedures (or heavily reduced)
- Clear separation of:
- Controller layer (REST APIs)
- Service layer (business rules)
- Repository/DAO layer (DB access)
- CI/CD pipeline: build, test, static analysis, deploy
GitLab & GitLab Duo in this target state:
- GitLab: single platform for repos, issues, CI/CD, environments
- GitLab Duo:
- Explains complex legacy code (PL/SQL and Java)
- Suggests Java implementation based on comments or PL/SQL snippets
- Generates or improves unit/integration tests
- Assists with
.gitlab-ci.ymlpipelines and MR reviews
3. Migration Strategy: Incremental, Risk-Aware, AI-Assisted
A sensible approach for regulated BFSI environments:
3.1 Strangler-Fig Pattern vs Big-Bang Rewrite
- Big-bang rewrite: migrate all PL/SQL logic to Java at once
- High risk, long duration, large “blackout” window
- Strangler-fig pattern: gradually route new/selected functionalities to new services
- Start with well-bounded use cases (e.g., fund transfer, premium calculation)
- Coexistence: some logic in PL/SQL, some in Java
- Gradually “strangle” the old monolith and procedures
In BFSI, strangler-fig is usually preferred due to risk, audit, and stability concerns.
3.2 Prioritising Stored Procedures to Migrate
Typical ranking criteria:
- Business criticality (e.g., payments, FNOL for insurance)
- Change frequency (logic that changes often benefits more from Java)
- Complexity (start with medium, not the most complex)
- Dependencies (how many other modules call it)
- Performance profile (procedures causing DB hotspots)
GitLab Duo helps here by:
- Summarising large PL/SQL packages: “Explain what
PKG_FUNDS_TRANSFERdoes” - Mapping procedure dependencies: “List procedures in this package and who calls them”
- Identifying candidate boundaries: “Suggest logical services from this package”
3.3 Coexistence Phase
During migration:
- Some flows still call PL/SQL.
- Some flows call new Java services that work directly with tables.
- For safety, you may:
- Keep PL/SQL contracts for external systems while re-implementing logic in Java behind them.
- Use feature flags to toggle between old/new paths.
- Run shadow traffic to Java services to validate outputs vs legacy.
4. End-to-End Example: Fund Transfer from PL/SQL to Java Service
Let’s walk through a concrete example common in banks:
Use Case: Internal fund transfer between two accounts in the same bank.
4.1 The Legacy Oracle Stored Procedure
Simplified PL/SQL:
CREATE OR REPLACE PROCEDURE SP_TRANSFER_FUNDS (
p_from_account IN VARCHAR2,
p_to_account IN VARCHAR2,
p_amount IN NUMBER,
p_txn_id OUT VARCHAR2,
p_status_code OUT VARCHAR2,
p_status_message OUT VARCHAR2
) AS
v_from_balance NUMBER;
v_to_balance NUMBER;
v_new_txn_id VARCHAR2(50);
BEGIN
-- 1. Check source account balance
SELECT balance
INTO v_from_balance
FROM accounts
WHERE account_no = p_from_account
FOR UPDATE;IF v_from_balance < p_amount THEN p_status_code := 'INSUFFICIENT_FUNDS'; p_status_message := 'Insufficient balance in source account'; RETURN; END IF; -- 2. Lock destination account and get balance SELECT balance INTO v_to_balance FROM accounts WHERE account_no = p_to_account FOR UPDATE; -- 3. Debit source account UPDATE accounts SET balance = balance - p_amount WHERE account_no = p_from_account; -- 4. Credit destination account UPDATE accounts SET balance = balance + p_amount WHERE account_no = p_to_account; -- 5. Insert into transactions table v_new_txn_id := 'TXN_' || TO_CHAR(SYSDATE, 'YYYYMMDDHH24MISS') || '_' || p_from_account; INSERT INTO account_transactions ( txn_id, from_account, to_account, amount, txn_date, status ) VALUES ( v_new_txn_id, p_from_account, p_to_account, p_amount, SYSDATE, 'SUCCESS' ); -- 6. Commit done by caller or AUTOCOMMIT (depending on design) p_txn_id := v_new_txn_id; p_status_code := 'SUCCESS'; p_status_message := 'Funds transferred successfully';EXCEPTION WHEN NO_DATA_FOUND THEN p_status_code := 'ACCOUNT_NOT_FOUND'; p_status_message := 'Source or destination account not found'; WHEN OTHERS THEN p_status_code := 'SYSTEM_ERROR'; p_status_message := SQLERRM; END SP_TRANSFER_FUNDS; /
What this does:
- Locks and checks source account balance.
- Locks destination account.
- Debits and credits the accounts.
- Records a transaction.
- Returns a transaction ID and status.
In many banks, dozens of channels and downstream systems rely on this procedure.
4.2 Using GitLab Duo to Understand the Legacy Logic
A developer could:
- Open the PL/SQL file in the IDE integrated with GitLab / GitLab Duo.
- Ask: “Explain what
SP_TRANSFER_FUNDSdoes, step by step, including error handling and concurrency considerations.”
Duo can:
- Summarise the procedure in plain language.
- Highlight potential issues (e.g., what if
p_amount <= 0? what about currency?). - Help you document the procedure — valuable in regulated environments.
5. Designing the Java Service
Implement a Spring Boot service:
/api/v1/payments/transfer
Layers:
- DTOs: request/response objects
- Entities:
Account,AccountTransaction - Repository interfaces (Spring Data JPA)
- Service: business logic
- Controller: REST API
5.1 Domain Entities
// Account.java
package com.bank.payments.domain;import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Version; import java.math.BigDecimal;@Entity public class Account { @Id private String accountNo; private BigDecimal balance; @Version private Long version; // optimistic locking // getters and setters omitted for brevity }
// AccountTransaction.java
package com.bank.payments.domain;import jakarta.persistence.Entity; import jakarta.persistence.Id; import java.math.BigDecimal; import java.time.OffsetDateTime;@Entity public class AccountTransaction { @Id private String txnId; private String fromAccount; private String toAccount; private BigDecimal amount; private OffsetDateTime txnDate; private String status; // getters and setters omitted }
5.2 Repositories
// AccountRepository.java
package com.bank.payments.repository;import com.bank.payments.domain.Account; import org.springframework.data.jpa.repository.JpaRepository;public interface AccountRepository extends JpaRepository<Account, String> { }
// AccountTransactionRepository.java
package com.bank.payments.repository;import com.bank.payments.domain.AccountTransaction; import org.springframework.data.jpa.repository.JpaRepository;public interface AccountTransactionRepository extends JpaRepository<AccountTransaction, String> { }
You can ask GitLab Duo to:
- Generate repository interfaces.
- Suggest query methods if you need custom lookups.
- Highlight missing indexes or performance considerations based on queries.
5.3 DTOs and Exceptions
// TransferRequest.java
package com.bank.payments.api.dto;import java.math.BigDecimal;public class TransferRequest { private String fromAccount; private String toAccount; private BigDecimal amount; // getters and setters }
// TransferResponse.java
package com.bank.payments.api.dto;public class TransferResponse { private String txnId; private String statusCode; private String statusMessage; // constructors, getters, setters }
// BusinessException.java
package com.bank.payments.service;public class BusinessException extends RuntimeException { private final String statusCode; public BusinessException(String statusCode, String message) { super(message); this.statusCode = statusCode; } public String getStatusCode() { return statusCode; } }
5.4 Service Layer with Transactions
// FundsTransferService.java
package com.bank.payments.service;import com.bank.payments.api.dto.TransferRequest; import com.bank.payments.api.dto.TransferResponse; import com.bank.payments.domain.Account; import com.bank.payments.domain.AccountTransaction; import com.bank.payments.repository.AccountRepository; import com.bank.payments.repository.AccountTransactionRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.UUID;@Service public class FundsTransferService { private final AccountRepository accountRepository; private final AccountTransactionRepository txnRepository; public FundsTransferService(AccountRepository accountRepository, AccountTransactionRepository txnRepository) { this.accountRepository = accountRepository; this.txnRepository = txnRepository; } @Transactional public TransferResponse transfer(TransferRequest request) { validateRequest(request); Account from = accountRepository.findById(request.getFromAccount()) .orElseThrow(() -> new BusinessException("ACCOUNT_NOT_FOUND", "Source account not found")); Account to = accountRepository.findById(request.getToAccount()) .orElseThrow(() -> new BusinessException("ACCOUNT_NOT_FOUND", "Destination account not found")); if (from.getBalance().compareTo(request.getAmount()) < 0) { throw new BusinessException("INSUFFICIENT_FUNDS", "Insufficient balance in source account"); } // Debit source from.setBalance(from.getBalance().subtract(request.getAmount())); // Credit destination to.setBalance(to.getBalance().add(request.getAmount())); accountRepository.save(from); accountRepository.save(to); String txnId = "TXN_" + UUID.randomUUID(); AccountTransaction txn = new AccountTransaction(); txn.setTxnId(txnId); txn.setFromAccount(from.getAccountNo()); txn.setToAccount(to.getAccountNo()); txn.setAmount(request.getAmount()); txn.setTxnDate(OffsetDateTime.now(ZoneOffset.UTC)); txn.setStatus("SUCCESS"); txnRepository.save(txn); return new TransferResponse(txnId, "SUCCESS", "Funds transferred successfully"); } private void validateRequest(TransferRequest request) { if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) { throw new BusinessException("INVALID_AMOUNT", "Amount must be greater than zero"); } if (request.getFromAccount().equals(request.getToAccount())) { throw new BusinessException("INVALID_ACCOUNTS", "Source and destination must differ"); } } }
GitLab Duo can help here by:
- Generating the initial skeleton of
FundsTransferServicefrom comments or a description. - Suggesting validations you may have missed (e.g., null checks, limits).
- Refactoring large methods into smaller ones (
validateRequest,createTransaction, etc.).
5.5 Controller Layer
// FundsTransferController.java
package com.bank.payments.api;import com.bank.payments.api.dto.TransferRequest; import com.bank.payments.api.dto.TransferResponse; import com.bank.payments.service.BusinessException; import com.bank.payments.service.FundsTransferService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*;@RestController @RequestMapping("/api/v1/payments") public class FundsTransferController { private final FundsTransferService transferService; public FundsTransferController(FundsTransferService transferService) { this.transferService = transferService; } @PostMapping("/transfer") public ResponseEntity<TransferResponse> transfer(@RequestBody TransferRequest request) { TransferResponse response = transferService.transfer(request); return ResponseEntity.ok(response); } @ExceptionHandler(BusinessException.class) public ResponseEntity<TransferResponse> handleBusinessException(BusinessException ex) { TransferResponse response = new TransferResponse(null, ex.getStatusCode(), ex.getMessage()); HttpStatus status = "INSUFFICIENT_FUNDS".equals(ex.getStatusCode()) ? HttpStatus.BAD_REQUEST : HttpStatus.UNPROCESSABLE_ENTITY; return new ResponseEntity<>(response, status); } @ExceptionHandler(Exception.class) public ResponseEntity<TransferResponse> handleGenericException(Exception ex) { TransferResponse response = new TransferResponse(null, "SYSTEM_ERROR", "Internal server error"); return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } }
Again, GitLab Duo can:
- Suggest controller boilerplate.
- Propose consistent error-response structures.
- Help extract common exception handling into a
@ControllerAdvice.
6. Testing Strategy: A Big Win When Moving to Java
In BFSI, testing and auditability are critical.
Get Shailesh Gogate’s stories in your inbox
Join Medium for free to get updates from this writer.
With business logic in Java, you can:
- Write unit tests for the service layer without hitting the database.
- Write integration tests using an in-memory DB or test schema.
- Automate tests in GitLab CI.
6.1 Example Unit Test
// FundsTransferServiceTest.java
package com.bank.payments.service;import com.bank.payments.api.dto.TransferRequest; import com.bank.payments.domain.Account; import com.bank.payments.repository.AccountRepository; import com.bank.payments.repository.AccountTransactionRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test;import java.math.BigDecimal; import java.util.Optional;import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*;class FundsTransferServiceTest { private AccountRepository accountRepository; private AccountTransactionRepository txnRepository; private FundsTransferService service; @BeforeEach void setUp() { accountRepository = mock(AccountRepository.class); txnRepository = mock(AccountTransactionRepository.class); service = new FundsTransferService(accountRepository, txnRepository); } @Test void transfer_successful() { Account from = new Account(); from.setAccountNo("A1"); from.setBalance(new BigDecimal("1000.00")); Account to = new Account(); to.setAccountNo("A2"); to.setBalance(new BigDecimal("500.00")); when(accountRepository.findById("A1")).thenReturn(Optional.of(from)); when(accountRepository.findById("A2")).thenReturn(Optional.of(to)); TransferRequest request = new TransferRequest(); request.setFromAccount("A1"); request.setToAccount("A2"); request.setAmount(new BigDecimal("200.00")); var response = service.transfer(request); assertEquals("SUCCESS", response.getStatusCode()); assertEquals(new BigDecimal("800.00"), from.getBalance()); assertEquals(new BigDecimal("700.00"), to.getBalance()); verify(txnRepository, times(1)).save(any()); } @Test void transfer_insufficientFunds() { Account from = new Account(); from.setAccountNo("A1"); from.setBalance(new BigDecimal("100.00")); Account to = new Account(); to.setAccountNo("A2"); to.setBalance(new BigDecimal("500.00")); when(accountRepository.findById("A1")).thenReturn(Optional.of(from)); when(accountRepository.findById("A2")).thenReturn(Optional.of(to)); TransferRequest request = new TransferRequest(); request.setFromAccount("A1"); request.setToAccount("A2"); request.setAmount(new BigDecimal("200.00")); BusinessException ex = assertThrows(BusinessException.class, () -> service.transfer(request)); assertEquals("INSUFFICIENT_FUNDS", ex.getStatusCode()); } }
How GitLab Duo helps with tests:
- Generate initial test methods based on
FundsTransferServicelogic. - Suggest additional edge cases (zero amount, same account, missing account).
- Help mock dependencies and make assertions consistent.
6.2 CI Pipeline with GitLab
A simple .gitlab-ci.yml snippet:
stages:
- build
- testmaven-build: stage: build script: - mvn -B clean compilemaven-test: stage: test script: - mvn -B test artifacts: when: always reports: junit: target/surefire-reports/*.xml
You can ask GitLab Duo:
“Update
.gitlab-ci.ymlto add code coverage reports and run integration tests with profileit.”
It can propose changes, and then you adjust for your organisation’s standards.
7. Key Technical Topics in BFSI Context
7.1 Transactions
- PL/SQL often relies on implicit transactions or DB-level control.
- In Java, you use Spring’s
@Transactionalto: - Define transaction boundaries clearly.
- Integrate with distributed transactions (if needed).
- For BFSI, ensure:
- Idempotence for critical operations (retries).
- Proper isolation levels for concurrent transfers.
GitLab Duo can help:
- Spot missing
@Transactionalannotations. - Suggest transaction boundaries based on methods that perform multiple updates.
7.2 Validation & Business Rules
- In PL/SQL, validations are often spread across IFs and nested blocks.
- In Java, you can:
- Use bean validation (
@NotNull,@Positive) for input. - Encapsulate rules in dedicated services or domain objects.
For insurance:
- Premium or claim calculations can be extracted into dedicated pricing or claims services.
GitLab Duo can:
- Extract repeated validation logic into shared helper methods.
- Suggest where to introduce enum types or value objects for clarity (e.g.,
TransactionStatus,Currency).
7.3 Error Handling & Logging
- PL/SQL uses
EXCEPTIONblocks and error codes. - Java uses exceptions, error codes, and structured logs.
Recommendations:
- Map PL/SQL status codes into domain-specific error codes in Java.
- Standardize error responses for APIs (HTTP 4xx/5xx with JSON bodies).
- Log with correlation IDs for audit trails (critical in BFSI).
GitLab Duo can:
- Help you design a consistent error model.
- Suggest improvements to logging statements and their context.
7.4 Performance & Concurrency
When moving logic out of the DB:
- Watch for extra round trips to the database.
- Use batch operations where appropriate.
- Consider caching for read-heavy operations (e.g., static interest rate tables).
On the DB side:
- Consider optimistic locking (as shown with
@Version) or pessimistic locks when needed. - Ensure proper indexing and query optimization remain in place.
GitLab Duo can:
- Flag potentially N+1 query patterns.
- Suggest index hints or query improvements (though DBAs should still validate).
8. Before vs After: Modernisation Snapshot
Press enter or click to view image in full size

9. Conclusion and Lessons Learned
Modernising Oracle stored-procedure–based applications into Java services is not just a technology upgrade. In BFSI, it’s about:
- Making core banking and insurance logic more transparent, testable, and auditable
- Enabling faster change while preserving safety and compliance
- Preparing for open banking, embedded insurance, and cloud-native deployments
GitLab Duo doesn’t replace architects and developers — it amplifies them:
- Helps understand complex PL/SQL logic and legacy Java code
- Accelerates scaffolding of new services, DTOs, and tests
- Assists in evolving CI/CD pipelines and reviewing merge requests
Practical Tips for BFSI Teams
- Start with a well-bounded use case
Pick something like fund transfer, premium calculation, or claim status — not the entire core system. - Use GitLab Duo early, for analysis
Upload PL/SQL packages and ask Duo to explain, summarise, and propose candidate service boundaries. - Mirror existing rules first, then improve
Ensure the Java service behaves identically to the stored procedure (same error codes, edge cases) before refactoring. - Bake tests into the plan
Use GitLab Duo to generate initial tests, then beef them up. In BFSI, tests are your shield against regressions. - Document & standardize patterns
Once you have one “golden path” (like the fund transfer service), make it a reusable pattern: code templates, CI jobs, and guidelines that everyone follows.

