Milesoft Stripe Extension (stripe-ext)
1. Overview & Purpose
The Stripe Extension (stripe-ext) is a secure, lightweight, and standardized JVM facade around the official Stripe Java SDK. It ensures consistent payment-processing behaviors, precision-safe currency operations, and standardized session handling across all Milesoft enterprise applications.
By abstracting away raw Stripe API boilerplate, stripe-ext allows developers to easily initiate payment sequences, capture customer transaction states, and securely manage API keys.
Key Benefits & Core Capabilities:
- Precision-Safe Currency Management: Encapsulates transaction values inside a dedicated, integer-based
Moneydomain model, preventing float/double rounding errors during financial calculations. - Standardized Facade: Provides streamlined methods for generating PaymentIntents (
createClientSecret) and retrieving active checkout sessions (loadSession). - Secure Key Management: Integrates natively with the platform's
SecretManagerServicefor secure, zero-hardcoded secret API key retrieval. - Robust Exception Handling: Gracefully handles communication failures and Stripe-specific exceptions, returning typed Optionals or clean debug/error logs to avoid application context crashes.
2. Installation & Dependency Configuration
The Stripe Extension library is hosted inside our secure Google Cloud Artifact Registry repository.
To configure your Gradle application to resolve and pull this extension, configure the repositories and dependency blocks inside your build.gradle:
2.1 Gradle Setup (build.gradle)
plugins {
id "com.google.cloud.artifactregistry.gradle-plugin" version "2.2.5"
id "java"
}
repositories {
mavenLocal()
mavenCentral()
maven {
url "artifactregistry://us-west2-maven.pkg.dev/milesoft-repo/repo-maven"
}
}
dependencies {
// Required base platform utilities
implementation "io.milesoft:commons:2.0.1"
// Milesoft Stripe Integration Extension
implementation "io.milesoft:stripe-ext:2.0.1"
}
3. Configuring Stripe in Spring Boot
Downstream applications typically register StripeService as a Spring-managed bean, resolving the sensitive Stripe Secret API key from Google Cloud Secret Manager at startup.
3.1 Properties Configuration
In your properties file (e.g., application.yml or application.properties), define the target secret key stored in GCP Secret Manager:
secrets:
stripe:
# Key identifying the Stripe API key within GCP Secret Manager
apiKey: "prod-stripe-api-key"
3.2 Stripe Configuration Class
Create a @Configuration class to resolve the key via SecretManagerService and provision the singleton StripeService instance:
import static com.google.common.base.Preconditions.checkNotNull;
import io.milesoft.secrets.services.SecretManagerService;
import io.milesoft.stripe.services.StripeService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class StripeConfig {
@Bean
public StripeService stripeService(
SecretManagerService secretManagerService,
@Value("${secrets.stripe.apiKey}") String secretApiKey) {
checkNotNull(secretManagerService, "secretManagerService must not be null");
// Load the plain-text key dynamically from Google Cloud Secret Manager
final String apiKey = secretManagerService.loadSecret(secretApiKey);
return new StripeService(apiKey);
}
}
4. Domain & Data Types
The library enforces strict structural integrity on currency calculations and data exchange through custom domain classes.
4.1 Money Class
To completely eliminate precision bugs associated with floating-point types (float and double), stripe-ext implements a strict, integer-based money representation. It decomposes currency values into distinct dollars (long) and cents (int) properties.
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
@Getter
@EqualsAndHashCode
@ToString
public class Money {
private final long dollars;
private final int cents;
public Money(long dollars, int cents) {
// Enforce basic invariants
checkArgument(dollars >= 0, "dollars must be >= 0");
checkArgument(cents >= 0 && cents < 100, "cents must be >= 0 and < 100");
this.dollars = dollars;
this.cents = cents;
}
public boolean isFree() {
return dollars == 0 && cents == 0;
}
public boolean isMoreThan(Money money) {
checkNotNull(money, "money must not be null");
return this.dollars > money.getDollars()
|| (this.dollars == money.getDollars() && this.cents > money.getCents());
}
public boolean isLessThan(Money money) {
checkNotNull(money, "money must not be null");
return this.dollars < money.getDollars()
|| (this.dollars == money.getDollars() && this.cents < money.getCents());
}
}
5. API Reference Guide
5.1 StripeService
A thread-safe, high-performance service orchestrating Stripe interactions.
Constructor:
public StripeService(String apiKey)
apiKey: The plain-text Stripe API key. Instantly configures the staticStripe.apiKeyvariable. Must not be blank.
Core Methods:
| Method Signature | Return Type | Description |
|---|---|---|
createClientSecret(Money money) |
String |
Initiates an authenticated PaymentIntent via Stripe with the specified amount, targeting the USD currency. Returns the unique clientSecret used by frontend clients. |
loadSession(String sessionId) |
Optional<Session> |
Retrieves a full checkout Session object. Catches StripeException and logs warnings gracefully rather than throwing errors. |
toAmount(Money money) |
long |
Helper. Package-private method that transforms a Money instance into a raw cents value (e.g., $49.99 $\rightarrow$ 4999) as required by Stripe's underlying APIs. |
6. Practical Real-World Code Examples
These production-inspired patterns demonstrate how the frontend and backend communicate securely using stripe-ext during different checkout paradigms.
6.1 Flow A: Custom Element Checkout (Direct PaymentIntent)
Ideal for modular checkouts embedded directly on your pages using Stripe Elements.
1. Backend Controller (StripeController.java)
Exposes an endpoint to calculate pricing and generate the client secret:
import static com.google.common.base.Preconditions.checkNotNull;
import com.yourcompany.myapp.api.v1.dto.Money;
import com.yourcompany.myapp.api.v1.dto.Stripe;
import io.milesoft.stripe.services.StripeService;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/stripe")
public class StripeController {
private final StripeService stripeService;
public StripeController(StripeService stripeService) {
this.stripeService = checkNotNull(stripeService, "stripeService must not be null");
}
@PostMapping(path = "/init", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public Stripe init(@RequestBody @Valid @NotNull Money request) {
// Map DTO to domain Money representation
final io.milesoft.stripe.domain.Money money = new io.milesoft.stripe.domain.Money(request.getDollars(), request.getCents());
// Initiate Stripe PaymentIntent
final String clientSecret = stripeService.createClientSecret(money);
final Stripe response = new Stripe();
response.setClientSecret(clientSecret);
return response;
}
}
2. Frontend React Integration (stripe.js)
Invokes the init endpoint to retrieve the secret and initialize the payment form:
import axios from 'axios';
/**
* Request a Client Secret for custom Stripe Checkout Elements
*/
export const init = (money, success, failure) => {
const url = "/api/v1/stripe/init";
axios.post(url, {
dollars: money.dollars,
cents: money.cents,
})
.then(response => {
const json = response.data;
success(json.clientSecret);
})
.catch(err => {
failure(err);
});
};
6.2 Flow B: Stripe Hosted Checkout (Session Fulfill & Verification)
Ideal for standard, SaaS-style checkout flows redirecting to checkout.stripe.com, followed by an asynchronous hook or success redirect.
1. Backend Controller (StripeController.java)
Verifies session success details, retrieves purchase items, and fulfills the purchase:
import static com.google.common.base.Preconditions.checkNotNull;
import com.stripe.exception.StripeException;
import com.stripe.model.LineItem;
import com.stripe.model.checkout.Session;
import com.stripe.param.checkout.SessionListLineItemsParams;
import io.milesoft.stack.exceptions.PaymentRequiredException;
import io.milesoft.stripe.services.StripeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
@Slf4j
@RestController
@RequestMapping("/api/v1/stripe")
public class StripeController {
private final StripeService stripeService;
public StripeController(StripeService stripeService) {
this.stripeService = checkNotNull(stripeService, "stripeService must not be null");
}
@GetMapping(path = "/resolve")
public StripeUser resolve(@RequestParam String sessionId) {
// Load the session detail from Stripe using the SDK extension
final Optional<Session> optional = stripeService.loadSession(sessionId);
if (optional.isPresent()) {
final Session session = optional.get();
final Session.CustomerDetails details = session.getCustomerDetails();
StripeUser response = new StripeUser();
response.setEmail(details.getEmail());
response.setPhone(details.getPhone());
return response;
} else {
throw new PaymentRequiredException("Unable to confirm payment: " + sessionId);
}
}
@PostMapping(path = "/fulfill")
public AccountUser fulfill(@RequestBody SaveStripeUser request) {
final String sessionId = request.getSessionId();
final Optional<Session> optional = stripeService.loadSession(sessionId);
if (optional.isPresent()) {
final Session session = optional.get();
try {
final SessionListLineItemsParams params = SessionListLineItemsParams.builder()
.addExpand("data.price.product")
.build();
// Inspect purchased items in the checkout session
for (LineItem item : session.listLineItems(params).getData()) {
log.info("Purchased item: {}, price ID: {}", item.getDescription(), item.getPrice().getId());
}
} catch (StripeException e) {
log.error("Unable to retrieve line items for session {}", sessionId, e);
}
// Execute enrollment or workspace provisioning logic...
return accountController.provisionNewAccount(request);
} else {
throw new PaymentRequiredException("Unable to confirm payment: " + sessionId);
}
}
}
2. Frontend React Integration (stripe.js)
Frontend hooks triggered in the Redirect/Thank You landing page:
import axios from "axios";
export const resolve = (sessionId, success, failure) => {
axios.get("/api/v1/stripe/resolve?sessionId=" + sessionId)
.then(response => {
success(response.data);
})
.catch(err => {
failure(err);
});
};
export const fulfill = (sessionId, user, success, failure) => {
axios.post("/api/v1/stripe/fulfill", {
sessionId: sessionId,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
phone: user.phone,
password: user.password,
})
.then(response => {
success(response.data);
})
.catch(err => {
failure(err);
});
};
7. Troubleshooting & FAQs
Q: Why is Stripe.apiKey set statically inside StripeService?
A: Stripe's underlying official Java SDK is historically designed to configure the API key as a static configuration variable (Stripe.apiKey). Consequently, the constructor of StripeService writes directly to this static value. If your application handles multiple distinct Stripe keys (e.g., multi-tenant setups), ensure you serialize calls or isolate container contexts properly.
Q: Does the library support non-USD currencies?
A: By design, StripeService.createClientSecret locks the currency format to StripeConstants.CURRENCY_US_DOLLAR ("USD") to safeguard consistency across current platform deployments. To support alternative currencies, you can clone or extend the transaction initialization logic inside StripeService.
Q: How can I run local integration tests without hitting Stripe's real API?
A: TestStripeService contains tests that run directly against real Stripe endpoints using keys loaded from /application.private.properties. Because of this external reliance, these tests are decorated with JUnit's @Ignore annotation to prevent CI/CD runner blockage. For local testing, copy /src/main/resources/application.private.properties to your root classpath and fill in a valid stripe.apiKey, then remove the @Ignore annotation to verify network connectivity.