π³ Integrating Razorpay Payment Gateway into My Java Spring Boot E-Commerce Project
Building an e-commerce application is not just about displaying products and adding them to a cart.
At some point, the application needs to handle one of the most important parts of the entire flow:
Payments.

While working on my ShopEase, a full-stack e-commerce application built with Java Spring Boot, MySQL, JavaScript and JWT authentication, I recently implemented Razorpay Test Mode payments.
The implementation looked straightforward at first:
Checkout
β
Create Order
β
Create Razorpay Order
β
Open Razorpay Checkout
β
Verify Payment
β
Confirm Order
But in practice, there were several problems that I had to debug and solve.
This post documents what I actually implemented, the problems I faced, and what I learned from them.
π About ShopEase
ShopEase is my full-stack e-commerce project.
Tech Stack
Frontend
HTML
CSS
JavaScript
Fetch API
LocalStorage
Backend
Java
Spring Boot
Spring MVC
Spring Data JPA
Hibernate
Spring Security
JWT
BCrypt
Database
MySQL
Payment Gateway
Razorpay
Tools
Eclipse
Postman
XAMPP
Git
GitHub
Before implementing payments, I already had:
User authentication
JWT authorization
Product system
Shopping cart
Wishlist
Checkout
Order creation
Order history
So the next logical step was integrating payments.
π― What I Wanted to Build
I didn't want Razorpay to simply open a payment popup.
I wanted a complete payment lifecycle.
The final architecture became:
User
β
Cart
β
Checkout
β
POST /orders
β
ShopEase Order Created
β
POST /payments/create
β
Razorpay Order Created
β
Razorpay Checkout
β
Payment
βββ SUCCESS
β β
β Verify Signature
β β
β Payment = SUCCESS
β β
β Order = CONFIRMED
β β
β Cart Cleared
β
βββ FAILURE
β
Payment = FAILED
β
Order = PENDING
β
Cart Preserved
This distinction between ShopEase Order and Razorpay Order was one of the most important concepts I learned during the implementation.
π§© Step 1 β Creating a Razorpay Order
I created a backend endpoint:
POST /payments/create
The frontend sends:
orderId
amount
The backend first finds the ShopEase order.
Then I validate that the payment amount matches the actual order amount.
if (!order.getTotalAmount().equals(amount)) {
throw new RuntimeException(
"Payment amount does not match order amount"
);
}
I then convert the amount into paise:
int amountInPaise =
(int) Math.round(amount * 100);
and create the Razorpay order.
The Razorpay order ID is then stored in my Payment entity.
This gives me a relationship like:
ShopEase Order
β
Payment
β
Razorpay Order ID
π Step 2 β Payment Verification
One of the most important parts of the integration was payment verification.
After successful payment, Razorpay provides:
razorpay_order_id
razorpay_payment_id
razorpay_signature
I send these values to:
POST /payments/verify
The backend constructs the signature payload:
String payload =
razorpayOrderId
+ "|"
+ razorpayPaymentId;
Then I verify the signature using the Razorpay secret:
Utils.verifySignature(
payload,
razorpaySignature,
System.getenv("RAZORPAY_KEY_SECRET")
);
I deliberately kept the secret on the backend instead of exposing it in JavaScript.
π Challenge 1 β Order ID Was Missing
This was one of the first real bugs I encountered.
The frontend initially expected the order response to directly contain the order ID.
But my backend response was wrapped inside my standard:
ApiResponse
The actual response structure was effectively:
{
"success": true,
"message": "Order placed successfully",
"data": {
"orderId": 14,
"totalAmount": 4299
}
}
My frontend initially wasn't extracting the nested data correctly.
This resulted in:
Order created but Order ID was not received.
I debugged it by printing the raw response:
console.log(
"RAW ORDER RESPONSE:",
JSON.stringify(order, null, 2)
);
The response showed me that I was reading the wrong level of the JSON structure.
I fixed the API layer so that placeOrder() returns:
return {
orderId: order.orderId,
totalAmount: order.totalAmount
};
This was a good reminder that frontend/backend integration bugs are often response-contract bugs, not business-logic bugs.
π Challenge 2 β Razorpay Was Not Opening Reliably
Another confusing problem was that Razorpay sometimes didn't open when I clicked Place Order from the cart flow.
But when I manually opened Checkout.html, it worked.
I initially suspected Razorpay itself.
The actual problem was in my application flow and state handling around the cart/checkout page.
I traced the complete flow instead of assuming the payment gateway was broken:
Cart
β
Checkout
β
Create Order
β
Create Razorpay Order
β
Open Razorpay
This debugging process helped me separate:
application bugs
from
payment gateway bugs.
That distinction saved a lot of time.
π Challenge 3 β Payment Failed But Database Still Had CREATED
This was a more important backend problem.
When a Razorpay payment failed, I initially had a record like:
Payment
Status = CREATED
even though the user had already failed the payment.
That meant my database wasn't representing the actual payment state.
I implemented a failure endpoint:
POST /payments/fail
and added logic to update the payment:
payment.setStatus(
PaymentStatus.FAILED
);
paymentRepository.save(payment);
The frontend listens for Razorpay's failure event:
razorpay.on(
"payment.failed",
async function(response) {
...
}
);
and records the failure on the backend.
After that, my database correctly reflected:
Payment β FAILED
π Challenge 4 β Failed Payment Was Clearing My Cart
This was probably the most important business-logic issue I encountered.
Initially, the order creation flow deleted cart items immediately after creating the order.
That produced this problem:
Cart
β
Create Order
β
Cart deleted β
β
Payment
β
Payment FAILED
Now the customer had paid nothing, but their cart was already empty.
That's bad e-commerce behavior.
β The Solution β Clear Cart Only After Successful Payment
I changed the flow so that creating an order does not immediately delete the cart.
Instead:
Order Created
β
Payment Created
β
Razorpay Payment
Only after successful payment verification:
Payment SUCCESS
β
Order CONFIRMED
β
Cart Cleared
The backend finds the user's cart:
Cart cart =
cartRepository
.findByUser(order.getUser())
.orElse(null);
Then retrieves its items:
List cartItems =
cartItemRepository
.findByCart(cart);
and deletes them only after successful payment.
This gives the correct behavior:
Successful payment
Payment = SUCCESS
Order = CONFIRMED
Cart = EMPTY
Failed payment
Payment = FAILED
Order = PENDING
Cart = PRESERVED
π§ Challenge 5 β Java Method/Brace Error
While modifying PaymentService, I also introduced a simple but annoying Java structure error.
I accidentally placed:
markPaymentAsFailed()
inside:
verifyPayment()
because a closing brace was missing.
The compiler then highlighted:
return true;
which initially made it look like return true itself was the problem.
The actual issue was the method structure.
The correct structure is:
verifyPayment() {
...
return true;
}
markPaymentAsFailed() {
...
}
This was a small bug, but it reinforced an important debugging lesson:
When Java highlights a perfectly valid line, don't automatically assume that line is the real problem. Check the surrounding structure first.
π Final Payment Flow
After fixing these issues, the complete flow became:
SHOP EASE
β
Cart
β
Checkout
β
Create Order
β
MySQL Order
β
Create Razorpay Order
β
Razorpay Checkout
/ \
/ \
SUCCESS FAILURE
β β
Verify Signature β
β β
Payment SUCCESS Payment FAILED
β β
Order CONFIRMED Order PENDING
β β
Clear Cart Keep Cart
π§ͺ Testing
I tested both important scenarios.
Successful Payment
Result:
Order ID: 20
Status: CONFIRMED
Total: βΉ3499
Payment: SUCCESS
Cart: Cleared
Failed Payment
Result:
Payment: FAILED
Order: PENDING
Cart: Preserved
This was important because testing only successful payments would have hidden the cart-loss bug.
π Security Considerations
I also made sure that sensitive payment information wasn't trusted blindly from the frontend.
The backend:
Validates the order amount
Creates the Razorpay order
Stores the Razorpay order ID
Verifies the Razorpay signature
Keeps the Razorpay secret on the backend
Updates payment status on the backend
Updates order status only after successful verification
The frontend is responsible for initiating the checkout experience, but the backend is responsible for trusting and recording the payment result.
π What I Learned
The biggest lesson wasn't actually how to call the Razorpay API.
It was understanding that payment integration is a state-management problem.
A payment can move through states like:
CREATED
β
SUCCESS
or:
CREATED
β
FAILED
And the order has its own state:
PENDING
β
CONFIRMED
These states need to remain consistent.
I also learned:
Never clear a cart before payment succeeds.
Always verify payment on the backend.
Don't expose payment secrets in frontend code.
Debug the complete frontend β API β service β database flow.
API response structure matters as much as business logic.
Always test failure scenarios, not just the happy path.
Payment integration is tightly connected to order and cart state.
π What's Next?
Razorpay completes another major part of ShopEase.
The next areas I want to work on are:
Admin Dashboard
User Management
Inventory Management
Automated Testing
Pagination
Sorting
Advanced Search
Docker
CI/CD
Deployment
Eventually, I want to take ShopEase from a learning project toward a more production-oriented architecture.
π― Final Thoughts
Implementing Razorpay wasn't just about adding a payment popup.
The real challenge was making sure that:
Payment
β
Order
β
Cart
all remain consistent.
The bugs I encounteredβmissing order IDs, incorrect response handling, failed payment state, cart deletion and Java method structure issuesβwere actually more valuable than simply getting the first successful payment.
That's what made this implementation useful as a development experience.
ShopEase now has a complete tested payment flow using Razorpay Test Mode, with backend verification, payment status tracking, order confirmation, failure handling and correct cart behavior.
ShopEase -Shitanshu Jha
Top comments (0)