Developing Fintech Apps: Using Core Java, Stripe API, and MySQL


Introduction

Building a fintech application from scratch is no small feat. When I embarked on creating FINTAL, a fintech solution. What started as a 4th-semester database project evolved into an industry-grade banking solution that handles real money transactions, multi-role authentication, and enterprise-level security.

In this comprehensive guide, I'll walk you through my journey of building FINTAL, sharing the challenges I faced, the solutions I implemented, and the lessons I learned along the way.


Why Java for Fintech? The Foundation That Matters

The following are the reasons:

  1. Cross-Platform Compatibility
    Java’s “write once, run anywhere” capability makes deployment across different systems seamless.

  2. Stability for Mission-Critical Systems
    Java is one of the most stable and reliable languages, trusted by global banks and financial institutions.

  3. Built-in Security Features
    Java's mature security model was crucial for handling sensitive financial data. The built-in security features, along with robust libraries like BCrypt for password hashing,

  4. Strongly Typed Language
    Its strict type system reduces bugs and improves code clarity, which is essential in financial applications where precision matters.

  5. Scalability and Performance

    The JVM's optimization capabilities and Java's multithreading support were essential for handling concurrent financial transactions safely.


System Architecture: Building on Solid Foundations

FINTAL follows a clean MVC (Model-View-Controller) architecture, ensuring maintainability and scalability. Here's how I structured the application:

  1. Database Design Philosophy

The heart of any fintech application lies in its database design.

I created a comprehensive schema with eight core tables:

  • Account: The central entity storing account details and real-time balances.

  • Customer: Complete customer profiles with KYC information.

KYC, which stands for Know Your Customer, is a process used by businesses, particularly in the financial sector, to verify the identity of their customers and assess the potential risks associated with them.

  • Admin: Stores the data of admins that are present in the system.

  • Staff: Complete staff profiles with detailed information.

  • Transaction: Immutable transaction records with full audit trails.

  • Bill: Bill management with CSV upload capabilities.

  • Branch: Multi-branch support for scalability

  • Loan Application: Complete loan processing workflow

  1. Multi-Role Architecture

One of FINTAL's standout features is its sophisticated role-based access system:

  • Admin Dashboard: Complete system oversight with staff and branch management, and comprehensive analytics.

  • Staff Interface: Customer account management, transaction assistance, and bill processing capabilities.

Customer Portals
FINTAL features two distinct customer portals, each tailored to specific user needs:

  • Beneficiary Portal: Designed for users who primarily manage and submit bills.

  • Standard Customer Portal: Built for regular customers, offering features like secure fund transfers, transaction history, personalized financial dashboards, and overall account management.


Implementing ACID Compliance: The Financial Integrity Challenge

ACID compliance isn't just a buzzword in fintech – it's a fundamental requirement. Here's how I implemented each principle:

  1. Atomicity: All-or-Nothing Transactions

Every financial operation in FINTAL is wrapped in transactions. For Example:

try {
    connection.setAutoCommit(false);

    // Debit from source account
    debitAccount(sourceAccountId, amount);

    // Credit to destination account
    creditAccount(destinationAccountId, amount);

    // Log transaction
    logTransaction(transactionDetails);

    connection.commit();
} catch (Exception e) {
    connection.rollback(); // Where everything becomes zero in case of failure
    throw new TransactionFailedException("Transaction failed: " + e.getMessage());
}
  1. Consistency: Data Integrity Rules

Database constraints and triggers ensure data consistency. For instance, account balances can never go negative, and all transactions must have corresponding entries in the transaction log.

  1. Isolation: Concurrent Transaction Safety

Using appropriate isolation levels prevented issues like dirty reads and phantom reads, crucial when multiple users might be accessing the same account simultaneously.

  1. Durability: Permanent Transaction Records

Once committed, transactions are permanently stored with comprehensive audit trails, ensuring regulatory compliance and data recovery capabilities.


Security: The Non-Negotiable Foundation

Security in fintech isn't optional – it's the foundation everything else builds upon:

  1. Password Security

BCrypt hashing ensures passwords are stored securely with salt, making them virtually impossible to crack even if the database is compromised.

  1. Session Management

Secure session handling prevents unauthorized access and includes automatic timeout mechanisms for inactive sessions.

  1. Input Validation

Every user input is validated and sanitized to prevent SQL injection, XSS attacks, and other common vulnerabilities.

  1. Audit Logging

Comprehensive logging tracks every action in the system, creating an immutable audit trail for compliance and security monitoring.


Challenges Faced and Solutions Implemented

Challenge 1: Transaction Consistency

Problem: Ensuring atomic operations across multiple database operations.

Solution: Implemented comprehensive transaction management with proper rollback mechanisms and database triggers for automatic balance updates.

Example Transfer Money Case:

conn = DriverManager.getConnection(URL, USER, PASSWORD);
conn.setAutoCommit(false);  // Begin transaction

// Debit sender
try (PreparedStatement debitStmt = conn.prepareStatement(
    "UPDATE ACCOUNT SET ACCOUNT_CURRENT_BALANCE = ACCOUNT_CURRENT_BALANCE - ? " +
    "WHERE ACCOUNT_NUMBER = ? AND ACCOUNT_CURRENT_BALANCE >= ?")) {
    debitStmt.setBigDecimal(1, amount);
    debitStmt.setString(2, senderAcc);
    debitStmt.setBigDecimal(3, amount);
    if (debitStmt.executeUpdate() != 1) throw new SQLException("Insufficient balance!");
}

// Credit receiver
try (PreparedStatement creditStmt = conn.prepareStatement(
    "UPDATE ACCOUNT SET ACCOUNT_CURRENT_BALANCE = ACCOUNT_CURRENT_BALANCE + ? WHERE ACCOUNT_NUMBER = ?")) {
    creditStmt.setBigDecimal(1, amount);
    creditStmt.setString(2, receiverAcc);
    if (creditStmt.executeUpdate() != 1) throw new SQLException("Receiver not found!");
}
// Log debit + credit
// ...
conn.commit();  // Commit only after all successful

If any steps fail:

conn.rollback();  //  Full rollback

Challenge 2: Payment Processing Reliability

Problem: Ensure users can complete Stripe payments despite potential user-side or network issues, while providing clear feedback and maintaining balance consistency.

Solution: Implemented Stripe Checkout with robust status polling, timeout handling, and graceful UI feedback to manage payment completions, failures, and user cancellations.

// 1. Check for missing API key
if (stripeApiKey == null || stripeApiKey.isEmpty()) {
    JOptionPane.showMessageDialog(null, "Stripe API key is missing.");
    return;
}

// 2. Open Stripe checkout
Session session = Session.create(params);
String checkoutUrl = session.getUrl();
Desktop.getDesktop().browse(new URI(checkoutUrl));

// 3. Poll Stripe status (max 1 min)
while (true) {
    Session session = Session.retrieve(sessionId);
    String paymentStatus = session.getPaymentStatus();
    String sessionStatus = session.getStatus();

    // 4. On success, update balance
    if ("paid".equals(paymentStatus) || "complete".equals(sessionStatus)) {
        boolean ok = accountController.handleAddStripeAmount(accountNumber);
        if (ok) {
            JOptionPane.showMessageDialog(null, "Payment Successful!");
            SwingUtilities.invokeLater(onSuccessCallback);
        }
        break;
    }

    // 5. On failure or cancel
    if ("expired".equals(sessionStatus) || "canceled".equals(sessionStatus)) {
        JOptionPane.showMessageDialog(null, "Payment Failed or Cancelled.");
        break;
    }

    // 6. Timeout after 1 min
    if (System.currentTimeMillis() - startTime > timeoutMillis) {
        JOptionPane.showMessageDialog(null, "Payment timed out. Please try again.");
        break;
    }

    Thread.sleep(intervalMillis);
}

Challenge 3: Concurrent User Access

Problem: Simultaneous access to the same account by multiple users risks race conditions.

Solution: Applied SERIALIZABLE transaction isolation level to enforce strict execution order and prevent conflicts.

Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
conn.setAutoCommit(false); // Start transaction
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); // Prevent race conditions

Challenge 4: Data Validation

Problem: Ensuring data integrity across complex financial operations.

Solution: Multi-layer validation – client-side for user experience, server-side for security, and database-level for final integrity checks.

Regex Patterns Used for Data Validation:

private static final Pattern EMAIL = Pattern.compile("^[\\w.+-]+@[\\w.-]+\\.[A-Za-z]{2,7}$");
private static final Pattern CNIC = Pattern.compile("^\\d{13}$");
private static final Pattern PHONE = Pattern.compile("^\\d{11}$");
private static final Pattern DOB = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$");

Validating Form Fields before Account Creation:

private String validateFields() {
    StringBuilder sb = new StringBuilder();

    if (nameField.getText().isBlank()) sb.append("Name required. ");
    if (!CNIC.matcher(cnicField.getText()).matches()) sb.append("Invalid CNIC. ");
    if (!EMAIL.matcher(emailField.getText()).matches()) sb.append("Invalid email. ");
    if (!PHONE.matcher(phoneField.getText()).matches()) sb.append("Invalid phone. ");
    if (!DOB.matcher(dobField.getText()).matches()) sb.append("DOB format invalid. ");
    else try { LocalDate.parse(dobField.getText()); } 
         catch (Exception e) { sb.append("Invalid DOB. "); }

    String pwd = String.valueOf(passwordField.getPassword());
    String confirmPwd = String.valueOf(confirmPasswordField.getPassword());

    if (pwd.isBlank()) sb.append("Password required. ");
    if (!pwd.equals(confirmPwd)) sb.append("Passwords do not match. ");

    return sb.toString();
}

Challenge 5: Keeping Branch Employee Counts in Sync

Problem: Each branch keeps track of how many staff members are assigned to it. When staff members are added, removed, or transferred between branches, this count should automatically update without requiring manual intervention.

Solution: MySQL Triggers

  1. Increment Employee Count on INSERT

When a new staff member is added, increment the corresponding branch's count.

DELIMITER $$

CREATE TRIGGER INCREMENT_EMPLOYEE_COUNT
AFTER INSERT ON STAFF
FOR EACH ROW
BEGIN
    UPDATE BRANCH
    SET BRANCH_EMPLOYEE_COUNT = BRANCH_EMPLOYEE_COUNT + 1
    WHERE BRANCH_ID = NEW.STAFF_BRANCH_ID;
END$$

DELIMITER ;
  1. Decrement Employee Count on DELETE

When a staff member is removed, reduce the count in that branch.

DELIMITER $$

CREATE TRIGGER DECREMENT_EMPLOYEE_COUNT
AFTER DELETE ON STAFF
FOR EACH ROW
BEGIN
    UPDATE BRANCH
    SET BRANCH_EMPLOYEE_COUNT = BRANCH_EMPLOYEE_COUNT - 1
    WHERE BRANCH_ID = OLD.STAFF_BRANCH_ID;
END$$

DELIMITER ;
  1. Adjust Count on UPDATE (Branch Transfer)

If a staff member switches from one branch to another, update both old and new branches.

DELIMITER $$

CREATE TRIGGER UPDATE_EMPLOYEE_COUNT
AFTER UPDATE ON STAFF
FOR EACH ROW
BEGIN
    IF OLD.STAFF_BRANCH_ID <> NEW.STAFF_BRANCH_ID THEN
        -- Decrease count from old branch
        UPDATE BRANCH
        SET BRANCH_EMPLOYEE_COUNT = BRANCH_EMPLOYEE_COUNT - 1
        WHERE BRANCH_ID = OLD.STAFF_BRANCH_ID;

        -- Increase count in new branch
        UPDATE BRANCH
        SET BRANCH_EMPLOYEE_COUNT = BRANCH_EMPLOYEE_COUNT + 1
        WHERE BRANCH_ID = NEW.STAFF_BRANCH_ID;
    END IF;
END$$

DELIMITER ;

Why Use Triggers?

  • Automation – No need to update counts in application logic.

  • Consistency – Branch data is always accurate.

  • Maintainability – Business rules are centralized in the DB layer.


Lessons Learned and Best Practices

  1. Security First, Always

Never compromise on security for convenience. Every decision should prioritize data protection and user safety.

  1. ACID Compliance is Non-Negotiable

Financial applications require absolute data integrity. Invest time in proper transaction management from the beginning.

  1. Error Handling is Critical

Comprehensive error handling and logging are essential for debugging and maintaining user trust.

  1. Test Everything

Financial applications require extensive testing. Automate testing wherever possible and include edge cases in your test suite.

  1. Documentation Matters

Maintain comprehensive documentation for code, APIs, and deployment procedures. Future you will thank present you.


Future Enhancements and Scalability

  1. Planned Improvements

  • Microservices architecture for better scalability

  • Real-time analytics dashboard

  • Mobile application development

  • Advanced fraud detection algorithms

  • Integration with additional payment gateways

  1. Scalability Considerations

  • Horizontal scaling strategies

  • Database sharding for large transaction volumes

  • Caching layer implementation

  • Load balancing configuration


Important URL’s

  1. LinkedIn Post: https://www.linkedin.com/posts/safi-io_javadevelopment-mysql-databasesystems-activity-7335757583149846528-qyQr (Comprehensive feature breakdown in a 10-minute explanatory video.)

  2. GitHub Repository: https://github.com/safi-io/Fintal


Connect with me

  1. Email: m.safi.ullah@outlook.com

  2. Github: https://github.com/safi-io/

Ready to build your fintech application? Start with a solid foundation, prioritize security, and never compromise on data integrity. The future of digital banking is in your hands.

0
Subscribe to my newsletter

Read articles from Muhammad Safiullah Khan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Muhammad Safiullah Khan
Muhammad Safiullah Khan

I am a third-year CS undergraduate student based in Pakistan. Currently, I am focused on sharpening my core skills in computing and programming. My ultimate goal is to become a technology agnostic software engineer. That's all for now!