Migrating a Fintech Investment Platform from PHP to Java (Spring Boot)

Β· updated Β· post business case php java spring boot fintech

Migrating a Fintech Investment Platform from PHP to Java (Spring Boot)

Project Overview

Legacy code can be a major challenge for fintech platforms, particularly when dealing with aging PHP systems that are difficult to maintain and scale. To address these issues, we refactored an investment platform from PHP to Java, leveraging Spring Boot and SOAP-based APIs for improved reliability, performance, and maintainability.

In this article, we will walk through the design patterns used, the project structure, and key code examplesβ€”all in a way that even those without deep software development experience can understand.

Key Objectives

Why Refactor from PHP to Java?

PHP is widely used for web applications but can present challenges when scaling enterprise-grade financial platforms. The main reasons for refactoring include:

βœ”οΈ Improved Maintainability – Java’s object-oriented nature makes managing large codebases easier.

βœ”οΈ Performance Optimization – Java is better suited for high-performance applications with large-scale transactions.

βœ”οΈ Better Security – Java provides strong security features, which are crucial in fintech applications.

βœ”οΈ Scalability – Spring Boot enables microservice architecture, allowing for better modularity and scaling.

To facilitate a smooth transition, we designed a new platform structure incorporating design patterns that promote clean, reusable, and scalable code. Also the API’s are designed in Java using SOAP protocol.

Full Project Template with SOAP API Implementation (Java + Spring Boot)

This comprehensive manual will guide you through building the new investment platform using Java (Spring Boot) with SOAP API endpoints and the design patterns we discussed.

πŸ“‚ Project Structure

investment-platform/
β”œβ”€β”€ src/
β”‚   └── main/
β”‚       └── java/
β”‚           └── com/
β”‚               └── investment/
β”‚                   β”œβ”€β”€ adapter/
β”‚                   β”‚   β”œβ”€β”€ BankAAdapter.java
β”‚                   β”‚   └── BankBAdapter.java
β”‚                   β”œβ”€β”€ config/
β”‚                   β”‚   └── SOAPConfig.java
β”‚                   β”œβ”€β”€ controller/
β”‚                   β”‚   └── InvestmentController.java
β”‚                   β”œβ”€β”€ facade/
β”‚                   β”‚   └── BankServiceFacade.java
β”‚                   β”œβ”€β”€ model/
β”‚                   β”‚   └── Client.java
β”‚                   β”œβ”€β”€ repository/
β”‚                   β”‚   └── ClientRepository.java
β”‚                   β”œβ”€β”€ service/
β”‚                   β”‚   β”œβ”€β”€ InvestmentService.java
β”‚                   β”‚   └── MarketDataService.java
β”‚                   β”œβ”€β”€ soap/
β”‚                   β”‚   β”œβ”€β”€ BankAPI.java
β”‚                   β”‚   └── SOAPClient.java
β”‚                   β”œβ”€β”€ strategy/
β”‚                   β”‚   β”œβ”€β”€ CompoundInterestStrategy.java
β”‚                   β”‚   └── SimpleInterestStrategy.java
β”‚                   └── InvestmentPlatformApplication.java
└── pom.xml

The project structure explained:

The investment-platform project follows a structured layered architecture where each folder serves a specific purpose. This approach separates concerns, making the system more scalable, maintainable, and testable.

Here’s what each folder does and why it exists:

1️⃣ adapter/ – Bank API Integration (Adapter Pattern)

πŸ“Œ Purpose: Contains classes that allow the platform to interact with different banks’ APIs by converting their responses into a standard format.

πŸ“Œ Why is it needed?

πŸ“Œ Example Files:

2️⃣ config/ – Configuration Settings

πŸ“Œ Purpose: Contains configuration-related files, such as SOAP API settings and database connection properties.

πŸ“Œ Why is it needed?

πŸ“Œ Example Files:

3️⃣ controller/ – REST API Endpoints

πŸ“Œ Purpose: Defines REST API endpoints that external clients (web apps, mobile apps) interact with.

πŸ“Œ Why is it needed?

πŸ“Œ Example Files:

4️⃣ facade/ – Simplifying Complex Bank Interactions (Facade Pattern)

πŸ“Œ Purpose: Acts as a single point of interaction for multiple banks.

πŸ“Œ Why is it needed?

πŸ“Œ Example Files:

5️⃣ model/ – Data Objects (Entities)

πŸ“Œ Purpose: Defines Java objects that represent database entities.

πŸ“Œ Why is it needed?

πŸ“Œ Example Files:

6️⃣ repository/ – Database Access (Repository Pattern)

πŸ“Œ Purpose: Defines database operations using Spring Data JPA.

πŸ“Œ Why is it needed?

πŸ“Œ Example Files:

7️⃣ service/ – Business Logic

πŸ“Œ Purpose: Contains the core logic of the investment platform, such as investment calculations and bank interactions.

πŸ“Œ Why is it needed?

πŸ“Œ Example Files:

8️⃣ soap/ – SOAP API Clients

πŸ“Œ Purpose: Handles SOAP-based API communication with external bank services.

πŸ“Œ Why is it needed?

πŸ“Œ Example Files:

9️⃣ strategy/ – Investment Calculation Strategies (Strategy Pattern)

πŸ“Œ Purpose: Contains different investment calculation methods, such as compound interest and simple interest.

πŸ“Œ Why is it needed?

πŸ“Œ Example Files:

πŸ”Ÿ InvestmentPlatformApplication.java – The Main Entry Point

πŸ“Œ Purpose: This is the main class where the Spring Boot application starts.

πŸ“Œ Why is it needed?

πŸ“Œ Example File:

By structuring the platform this way, we achieve:

βœ” Separation of Concerns – Each folder has a clear responsibility.
βœ” Scalability – New features can be added without modifying the entire system.
βœ” Maintainability – Developers can quickly understand and modify the project.

Key Takeaways from the Structure

Let’s see how the theory would be used in a real-world scenario.

1. Initialize a Spring Boot Project

Create the project using Spring Initializr:

pom.xml

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.investment</groupId>
    <artifactId>investment-platform</artifactId>
    <version>1.0.0</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web-services</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

βš™οΈ 2. Configuration

application.properties

server.port=8080

spring.datasource.url=jdbc:mysql://localhost:3306/investment_db
spring.datasource.username=root
spring.datasource.password=secret

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect

SOAPConfig.java

package com.investment.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;

@Configuration
public class SOAPConfig {

    @Bean
    public Jaxb2Marshaller marshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setContextPath("com.investment.soap");
        return marshaller;
    }
}

🟑 3. SOAP Client with Singleton Pattern

SOAPClient.java

package com.investment.soap;

import javax.xml.soap.*;

public class SOAPClient {
    private static SOAPClient instance;
    private SOAPConnection connection;

    private SOAPClient() throws SOAPException {
        SOAPConnectionFactory factory = SOAPConnectionFactory.newInstance();
        connection = factory.createConnection();
    }

    public static SOAPClient getInstance() throws SOAPException {
        if (instance == null) {
            synchronized (SOAPClient.class) {
                if (instance == null) {
                    instance = new SOAPClient();
                }
            }
        }
        return instance;
    }

    public SOAPMessage sendRequest(String endpointUrl, SOAPMessage request) throws SOAPException {
        return connection.call(request, endpointUrl);
    }
}

Why Singleton?

🟠 4. Adapter Pattern for Bank APIs

SOAP Interface for Banks

package com.investment.soap;

public interface BankAPI {
    String getAccountBalance(String accountId);
    String transferFunds(String fromAccount, String toAccount, double amount);
}

Bank A Adapter

package com.investment.adapter;

import com.investment.soap.BankAPI;

public class BankAAdapter implements BankAPI {
    @Override
    public String getAccountBalance(String accountId) {
        // Simulate SOAP request to Bank A
        return "<balance>10000.00</balance>";
    }

    @Override
    public String transferFunds(String fromAccount, String toAccount, double amount) {
        return "BankA Transfer Successful";
    }
}

Why Adapter Pattern?

🟑 5. Facade Pattern for Multiple Banks

BankServiceFacade.java

package com.investment.facade;

import com.investment.soap.BankAPI;

public class BankServiceFacade {
    private final BankAPI bankAAdapter;
    private final BankAPI bankBAdapter;

    public BankServiceFacade(BankAPI bankAAdapter, BankAPI bankBAdapter) {
        this.bankAAdapter = bankAAdapter;
        this.bankBAdapter = bankBAdapter;
    }

    public String getConsolidatedBalance(String accountId) {
        String balanceA = bankAAdapter.getAccountBalance(accountId);
        String balanceB = bankBAdapter.getAccountBalance(accountId);
        return "BankA: " + balanceA + ", BankB: " + balanceB;
    }
}

Why Facade Pattern?

🟒 6. Strategy Pattern for Investment Calculations

InvestmentStrategy.java

package com.investment.strategy;

public interface InvestmentStrategy {
    double calculateReturn(double principal, double rate, int years);
}

CompoundInterestStrategy.java

package com.investment.strategy;

public class CompoundInterestStrategy implements InvestmentStrategy {
    @Override
    public double calculateReturn(double principal, double rate, int years) {
        return principal * Math.pow((1 + rate / 100), years);
    }
}

SimpleInterestStrategy.java

package com.investment.strategy;

public class SimpleInterestStrategy implements InvestmentStrategy {
    @Override
    public double calculateReturn(double principal, double rate, int years) {
        return principal + (principal * rate * years / 100);
    }
}

Why Strategy Pattern?

🟀 7. Observer Pattern for Real-Time Market Updates

MarketDataService.java

package com.investment.service;

import java.util.ArrayList;
import java.util.List;

interface Observer {
    void update(String marketUpdate);
}

interface MarketData {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers(String update);
}

public class MarketDataService implements MarketData {
    private final List<Observer> observers = new ArrayList<>();

    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(String update) {
        for (Observer observer : observers) {
            observer.update(update);
        }
    }
}

🟠 8. Repository Pattern for Database Operations

Client Entity

package com.investment.model;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
@Data
public class Client {
    @Id
    private String id;
    private String name;
    private String investmentType;
}

Client Repository Interface

package com.investment.repository;

import com.investment.model.Client;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ClientRepository extends JpaRepository<Client, String> {
}

🟒 9. Service Layer

InvestmentService.java

package com.investment.service;

import com.investment.facade.BankServiceFacade;
import com.investment.strategy.InvestmentStrategy;
import org.springframework.stereotype.Service;

@Service
public class InvestmentService {
    private final BankServiceFacade bankServiceFacade;
    private InvestmentStrategy strategy;

    public InvestmentService(BankServiceFacade bankServiceFacade) {
        this.bankServiceFacade = bankServiceFacade;
    }

    public void setStrategy(InvestmentStrategy strategy) {
        this.strategy = strategy;
    }

    public double calculateReturn(double principal, double rate, int years) {
        return strategy.calculateReturn(principal, rate, years);
    }

    public String getAccountBalances(String accountId) {
        return bankServiceFacade.getConsolidatedBalance(accountId);
    }
}

🟑 10. REST Controller

InvestmentController.java

package com.investment.controller;

import com.investment.service.InvestmentService;
import com.investment.strategy.CompoundInterestStrategy;
import com.investment.strategy.SimpleInterestStrategy;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/investment")
public class InvestmentController {
    private final InvestmentService investmentService;

    public InvestmentController(InvestmentService investmentService) {
        this.investmentService = investmentService;
    }

    @GetMapping("/returns")
    public double calculateReturns(
            @RequestParam String type,
            @RequestParam double principal,
            @RequestParam double rate,
            @RequestParam int years) {

        if ("compound".equalsIgnoreCase(type)) {
            investmentService.setStrategy(new CompoundInterestStrategy());
        } else {
            investmentService.setStrategy(new SimpleInterestStrategy());
        }

        return investmentService.calculateReturn(principal, rate, years);
    }

    @GetMapping("/balances")
    public String getBalances(@RequestParam String accountId) {
        return investmentService.getAccountBalances(accountId);
    }
}

Running the Platform

Step 1: Start MySQL

docker run --name mysql-investment -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=investment_db -p 3306:3306 -d mysql:5.7

Step 2: Build and Run Spring Boot Application

./mvnw clean install
./mvnw spring-boot:run

Step 3: Test Endpoints

Conclusion