Integrating payment processing into your Spring Boot application can seem daunting, but with Stripe's robust API and Spring Boot's flexibility, it’s a manageable task. In this guide, we'll walk through the process of integrating Stripe payments into a Spring Boot application, focusing on a booking system example. We'll cover setting up Stripe, initiating a payment session, handling webhooks, and confirming payments.
- A Stripe account with API keys (secret key and webhook secret).
- A Spring Boot project set up with dependencies like
spring-boot-starter-web
and stripe-java
. - Basic knowledge of Spring Boot, REST APIs, and Java.
- Add the Stripe Java library to your
pom.xml
:
<dependency>
<groupId>com.stripe</groupId>
<artifactId>stripe-java</artifactId>
<version>25.6.0</version>
</dependency>
To use Stripe's API, you need to set up your Stripe secret key in the application. This key authenticates your API requests.
In your application.properties
or application.yml
, add:
stripe.secret.key=sk_test_your_stripe_secret_key
stripe.webhook.secret=whsec_your_webhook_secret
Create a configuration class to initialize the Stripe API with the secret key:
import com.stripe.Stripe;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Configuration
public class StripeConfig {
@Value("${stripe.secret.key}")
private String stripeSecretKey;
@PostConstruct
public void init() {
Stripe.apiKey = stripeSecretKey; // Set up Stripe API key
}
}
This ensures the Stripe API key is set when the application starts.
To process payments, you’ll create a Stripe Checkout Session, which redirects users to a Stripe-hosted payment page. Below is an example of initiating a payment for a booking.
Create a REST endpoint to initiate a payment for a booking:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/bookings")
public class BookingController {
private final BookingService bookingService;
public BookingController(BookingService bookingService) {
this.bookingService = bookingService;
}
@PostMapping("/{bookingId}/payments")
public ResponseEntity<BookingPaymentInitResponseDto> initiateBookingPayment(@PathVariable Long bookingId) {
BookingPaymentInitResponseDto response = new BookingPaymentInitResponseDto(bookingService.initiateBookingPayment(bookingId));
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
The BookingPaymentInitResponseDto
is a simple data transfer object (DTO) to return the Stripe session URL:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BookingPaymentInitResponseDto {
private String sessionUrl;
}
In the service layer, validate the booking, check user authorization, and create a Stripe Checkout Session:
import com.stripe.exception.StripeException;
import com.stripe.model.Customer;
import com.stripe.model.Session;
import com.stripe.param.CustomerCreateParams;
import com.stripe.param.SessionCreateParams;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class BookingService {
private final BookingRepository bookingRepository;
private final InventoryRepository inventoryRepository;
private final String frontendUrl;
public BookingService(BookingRepository bookingRepository, InventoryRepository inventoryRepository, @Value("${frontend.url}") String frontendUrl) {
this.bookingRepository = bookingRepository;
this.inventoryRepository = inventoryRepository;
this.frontendUrl = frontendUrl;
}
@Transactional(noRollbackFor = BookingExpiredException.class)
public String initiateBookingPayment(Long bookingId) {
Booking booking = bookingRepository.findById(bookingId)
.orElseThrow(() -> new ResourceNotFoundException("Booking Not Found With Id: " + bookingId));
User user = getCurrentUser(); // Assume this retrieves the authenticated user
if (!user.equals(booking.getUser())) {
throw new UnAuthorisedException("Booking Does Not Belong To This User With Id: " + user.getId());
}
if (hasBookingExpired(booking.getCreatedAt())) {
inventoryRepository.expireBooking(booking.getRoom().getId(),
booking.getCheckInDate(),
booking.getCheckOutDate(),
booking.getNumberOfRooms());
booking.setBookingStatus(BookingStatus.EXPIRED);
bookingRepository.save(booking);
throw new BookingExpiredException("Booking Has Been Already Expired");
}
String sessionUrl = getCheckoutSession(booking, frontendUrl + "/payment/success", frontendUrl + "/payment/failure");
booking.setBookingStatus(BookingStatus.PAYMENT_PENDING);
bookingRepository.save(booking);
return sessionUrl;
}
public String getCheckoutSession(Booking booking, String successUrl, String failureUrl) {
log.info("Creating session for booking with Id: {}", booking.getId());
User user = getCurrentUser();
try {
CustomerCreateParams customerParams = CustomerCreateParams.builder()
.setName(user.getName())
.setEmail(user.getEmail())
.build();
Customer customer = Customer.create(customerParams);
SessionCreateParams sessionParams = SessionCreateParams.builder()
.setMode(SessionCreateParams.Mode.PAYMENT)
.setBillingAddressCollection(SessionCreateParams.BillingAddressCollection.REQUIRED)
.setCustomer(customer.getId())
.setSuccessUrl(successUrl)
.setCancelUrl(failureUrl)
.addLineItem(
SessionCreateParams.LineItem.builder()
.setQuantity(Long.valueOf(booking.getNumberOfRooms()))
.setPriceData(
SessionCreateParams.LineItem.PriceData.builder()
.setCurrency("inr")
.setUnitAmount(booking.getAmount().multiply(BigDecimal.valueOf(100)).longValue())
.setProductData(
SessionCreateParams.LineItem.PriceData.ProductData.builder()
.setName(booking.getHotel().getName() + " : " + booking.getRoom().getType())
.setDescription("Booking ID: " + booking.getId())
.build()
)
.build()
)
.build()
)
.build();
Session session = Session.create(sessionParams);
booking.setPaymentSessionId(session.getId());
bookingRepository.save(booking);
log.info("Session created successfully for booking with Id: {}", booking.getId());
return session.getUrl();
} catch (StripeException e) {
throw new RuntimeException("Failed to create Stripe session", e);
}
}
}
This code:
- Validates the booking and user.
- Checks if the booking has expired.
- Creates a Stripe customer and a checkout session with details like the hotel name, room type, and amount (in INR, multiplied by 100 for Stripe’s cent-based system).
- Updates the booking status to
PAYMENT_PENDING
and saves the session ID.
Stripe uses webhooks to notify your application of payment events, such as a completed checkout session. Set up a webhook endpoint to handle these events.
import com.stripe.exception.SignatureVerificationException;
import com.stripe.model.Event;
import com.stripe.net.Webhook;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/webhook")
public class StripeWebhookController {
private final BookingService bookingService;
private final String endpointSecret;
public StripeWebhookController(BookingService bookingService, @Value("${stripe.webhook.secret}") String endpointSecret) {
this.bookingService = bookingService;
this.endpointSecret = endpointSecret;
}
@PostMapping("/payment")
public ResponseEntity<Void> capturePayments(@RequestBody String payload, @RequestHeader("Stripe-Signature") String sigHeader) {
try {
Event event = Webhook.constructEvent(payload, sigHeader, endpointSecret);
bookingService.capturePayment(event);
return ResponseEntity.noContent().build();
} catch (SignatureVerificationException e) {
throw new RuntimeException("Invalid webhook signature", e);
}
}
}
Add the webhook handling logic to the BookingService
:
import com.stripe.model.Session;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class BookingService {
// Other methods as above...
@Transactional
public void capturePayment(Event event) {
if ("checkout.session.completed".equals(event.getType())) {
Session session = (Session) event.getDataObjectDeserializer().getObject().orElse(null);
if (session == null) return;
String sessionId = session.getId();
Booking booking = bookingRepository.findByPaymentSessionId(sessionId)
.orElseThrow(() -> new ResourceNotFoundException("Booking not found for session Id: " + sessionId));
booking.setBookingStatus(BookingStatus.CONFIRMED);
bookingRepository.save(booking);
List<Inventory> lockReservedInventory = inventoryRepository.findAndLockReservedInventory(
booking.getRoom().getId(),
booking.getCheckInDate(),
booking.getCheckOutDate(),
booking.getNumberOfRooms());
inventoryRepository.confirmBooking(
booking.getRoom().getId(),
booking.getCheckInDate(),
booking.getCheckOutDate(),
booking.getNumberOfRooms());
log.info("Successfully confirmed the booking for Booking Id: {}", booking.getId());
} else {
log.warn("Unhandled event type: {}", event.getType());
}
}
}
This code:
- Verifies the webhook event using the Stripe webhook secret.
- Processes the
checkout.session.completed
event to confirm the booking. - Updates the booking status to
CONFIRMED
and adjusts inventory (e.g., decreases reserved rooms and increases booked rooms).
- Set Up Webhooks Locally: Use a tool like
ngrok
to expose your local server to Stripe’s webhook events. Configure the webhook URL in the Stripe Dashboard (e.g., https://your-ngrok-url/webhook/payment
). - Test Payment Flow:
- Create a booking and initiate a payment via the
/api/bookings/{bookingId}/payments
endpoint. - Use Stripe’s test card (e.g.,
4242 4242 4242 4242
) to complete the payment. - Verify that the webhook updates the booking status to
CONFIRMED
.
- Handle Errors: Test edge cases like expired bookings or invalid payments to ensure proper exception handling.
- Security: Store Stripe keys securely (e.g., in environment variables) and validate webhook signatures to prevent unauthorized requests.
- Error Handling: Handle Stripe exceptions gracefully and provide meaningful error messages to users.
- Logging: Log important events (e.g., session creation, payment confirmation) for debugging and auditing.
- Testing: Use Stripe’s test mode extensively before going live.
Integrating Stripe into a Spring Boot application is straightforward with the right setup. By following this guide, you can enable secure payment processing for your application, handle payment confirmations via webhooks, and manage booking statuses effectively.
Want to Master Spring Boot and Land Your Dream Job?
Struggling with coding interviews? Learn Data Structures & Algorithms (DSA) with our expert-led course. Build strong problem-solving skills, write optimized code, and crack top tech interviews with ease
Learn moreJava
Stripe
Payment integration
Stripe Integration With Spring Boot
Spring Boot Stripe Beginner Guide