(Spring) Booting Java to Accept Digital Payments with USDC
Now focusing my career on Fintech, I decided it was time to understand how to introduce web3 into an existing Java-based RESTful API.
It’s funny how a seemingly meaningless event in one’s life can lead to an unexpected change. For me, one of those events happened in July 2021 when my flight home was delayed by so much that I paid for my very first Uber.
I was so impressed by the experience that I wrote about the underlying payment processor in my “Leveraging Marqeta to Build a Payment Service in Spring Boot” article. I continued to dive deeper into the Marqeta platform, writing about how to create a rewards card and even an article about building a buy now, pay later payment solution.
It was at that point I started talking with Marqeta about becoming a part of their engineering team. About six weeks later, I hung up my consultant hat and started as a full-time employee of a Fintech-based market disruptor that has created the world’s first modern card issuing platform.
It’s been an exciting journey along a challenging, winding road. I love it!
In my spare time, I have continued to dive deeper into the world of web3, and I am always eager to learn more about Fintech too. So exploring where web3 and financial services intersect is natural for me!
For this article, I wanted to see how easy it is for a web2 developer to use Java in order to perform some Fintech transactions using web3 and USDC over the Ethereum blockchain. My plan is to use the Circle Java SDK, Java 17, and a Spring Boot 3 RESTful API.
About Circle and USDC
We can’t talk about USDC (USD Coin) without first talking about Circle, since they are the company that manages the stablecoin. Circle was founded in 2013 (around the same time as Marqeta) with a focus on peer-to-peer payments.
As a private company, Circle has reached some key milestones:
Circle Pay (mobile payments) allowed users to hold, send, and receive traditional fiat currencies. Later, they became a Bitcoin digital wallet service – allowing consumers to buy/sell Bitcoins.
By 2015, a Circle account could be funded in USD via US-issued Visa and Mastercard credit cards and debit cards. Similar functionality was extended to Europe a year later.
In 2018, Circle raised venture capital to establish USDC with the promise that its coin is backed by fully reserved assets.
USDC is a digital (crypto) currency pegged to (and backed by) the US dollar. Basically, this means that 1 USDC is always equal to 1 US dollar.
So why USDC then instead of US dollars?
USDC can be sent, in any amount, for just a couple dollars.
USDC can be sent globally, and nearly instantly.
Because USDC is a digital currency, it can be sent, received, and settled at any time. You don’t have to worry about banking hours.
And since USDC is digital – there’s an API and SDK to work with it (which is what we’ll explore in this article).
USDC is issued by a private entity (it’s not a central bank digital currency) and is primarily available as an Ethereum ERC-20 token.
With USDC, Circle aims to be a disruptor by giving customers the ability to avoid bank hours, processing times and costly fees – all while building a business with the USDC digital currency.
Using USDC to Make and Receive Payments with Java? Yes, Please!
For this publication, let’s assume your service caters to those interested in using USDC to perform financial transactions. From what you’ve observed, the current fees associated with using the Circle platform will allow your service to still be profitable.
Still trying to keep this scenario realistic, let’s also assume that your infrastructure has its roots in Java-based microservices written in Spring Boot. Your infrastructure supports a proven infrastructure of web2 applications and services.
To keep things simple, we will introduce a new service – called circle-sdk-demo
– which will act as the integration with the Circle platform. As a result, we will plan to explore the Java SDK by Circle – which is still currently in a beta state.
Demonstration
Before getting started with the service, we need to navigate to the Circle’s Developer site and obtain an API key for sandbox use:
https://app-sandbox.circle.com/signup/sandbox
All I had to do is fill out this form:
And then I received an API key for their sandbox:
Keep the API key value handy, as it will be required before we start our new service.
Creating a Spring Boot Service
For this demonstration, I thought I would plan to use Spring Boot 3 for the first time. While I used the Spring Initializr in IntelliJ IDEA, the results of my dependencies are noted in the following pom.xml
file for the circle-sdk-demo
service:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.circle</groupId>
<artifactId>circle</artifactId>
<version>0.1.0-beta.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Externalizing Circle Configuration
In order to externalize the API key value, the CircleConfigurationProperties
class was created:
@Data
@Validated
@Configuration("circleConfigurationProperties")
@ConfigurationProperties("circle")
public class CircleConfigurationProperties {
@NotBlank()
private String apiKey;
}
For reference, here is a copy of my application.yaml
configuration file:
circle:
api-key:
server:
port: 8585
error:
include-message: always
You can use this link to determine how to externalize the population of the circle.api-key
value with the API key created above. I certainly do not recommend populating the API key value directly into the configuration file above.
Next, I created the following CircleConfiguration
:
@Slf4j
@RequiredArgsConstructor
@Component
public class CircleConfiguration {
private final CircleConfigurationProperties circleConfigurationProperties;
@Bean
public Circle getCircle() {
log.info("=======================");
log.info("Initializing Circle SDK");
log.info("=======================");
log.info("basePath={}", Circle.SANDBOX_BASE_URL);
log.info("circle.api-key={}",
SecurityUtils.maskCredentialsRevealPrefix(
circleConfigurationProperties.getApiKey(), 7, '*'));
Circle circle = Circle.getInstance()
.setBasePath(Circle.SANDBOX_BASE_URL)
.setApiKey(circleConfigurationProperties.getApiKey());
log.info("circle={}", circle);
log.info("==================================");
log.info("Circle SDK Initialization Complete");
log.info("==================================");
return circle;
}
}
With the API key value set, starting the circle-sdk-demo
Spring Boot 3.x service appears as shown below:
Adding a Circle Integration Service
To communicate with the Circle platform, I created a simple pass-through service called CircleIntegrationService
:
@Slf4j
@Service
public class CircleIntegrationService {
private final BalancesApi balancesApi = new BalancesApi();
private final CryptoPaymentIntentsApi cryptoPaymentIntentsApi = new CryptoPaymentIntentsApi();
public ListBalancesResponse getBalances() throws ApiException {
ListBalancesResponse listBalancesResponse = balancesApi.listBalances();
log.info("listBalancesResponse={}", listBalancesResponse);
return listBalancesResponse;
}
public CreatePaymentIntentResponse createPayment(SimplePayment simplePayment) throws ApiException {
CreatePaymentIntentRequest createPaymentIntentRequest = new CreatePaymentIntentRequest(new PaymentIntentCreationRequest()
.idempotencyKey(UUID.randomUUID())
.amount(
new CryptoPaymentsMoney()
.amount(simplePayment.getAmount())
.currency(simplePayment.getCurrency())
)
.settlementCurrency(simplePayment.getSettlementCurrency())
.paymentMethods(
Collections.singletonList(
new PaymentMethodBlockchain()
.chain(Chain.ETH)
.type(PaymentMethodBlockchain.TypeEnum.BLOCKCHAIN)
)
));
CreatePaymentIntentResponse createPaymentIntentResponse = cryptoPaymentIntentsApi.createPaymentIntent(createPaymentIntentRequest);
log.info("createPaymentIntentResponse={}", createPaymentIntentResponse);
return createPaymentIntentResponse;
}
public GetPaymentIntentResponse getPayment(String id) throws ApiException {
UUID paymentIntentId = UUID.fromString(id);
log.info("paymentIntentId={} from id={}", paymentIntentId, id);
GetPaymentIntentResponse getPaymentIntentResponse = cryptoPaymentIntentsApi.getPaymentIntent(paymentIntentId);
log.info("getPaymentIntentResponse={}", getPaymentIntentResponse);
return getPaymentIntentResponse;
}
}
This service allows the following functionality to be performed:
Obtain a list of balances for my API key
Create a new payment
Get an existing payment by ID
Creating RESTful URIs
In the example scenario, the circle-sdk-demo
will act as middleware between my existing services and the Circle platform. Next, basic controllers were created for the following URIs:
GET /balances
POST /payments
GET /payments/{id}
For this example, I simply created the BalancesController
and PaymentsController
classes to house these URIs. A more realistic design would employ an API First approach, similar to what I noted in my “Exploring the API-First Design Pattern” publication.
circle-sdk-demo Service In Action
With the circle-sdk-demo
service running, I was able to perform some cURL commands against my local service, which interacted with the Circle platform via the Java SDK.
Getting a list of balances:
curl --location 'localhost:8585/balances'
Results in the following payload response from Circle:
{
"data": {
"available": [],
"unsettled": []
}
}
Creating a payment:
curl --location 'localhost:8585/payments' \
--header 'Content-Type: application/json' \
--data '{
"currency" : "USD",
"amount" : "1.67",
"settlement_currency": "USD"
}'
Results in the following payload response from Circle:
{
"data": {
"id": "60b9ff8b-f28c-40cf-9a1c-207d12a5350b",
"amount": {
"amount": "1.67",
"currency": "USD"
},
"amountPaid": {
"amount": "0.00",
"currency": "USD"
},
"amountRefunded": {
"amount": "0.00",
"currency": "USD"
},
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": null
}
],
"fees": null,
"paymentIds": [],
"refundIds": [],
"timeline": [
{
"status": "created",
"context": null,
"reason": null,
"time": "2023-03-28T12:26:39.607607Z"
}
],
"expiresOn": null,
"updateDate": "2023-03-28T12:26:39.604637Z",
"createDate": "2023-03-28T12:26:39.604637Z",
"merchantWalletId": "1013833795"
}
}
Getting an existing payment by ID:
curl --location 'localhost:8585/payments/60b9ff8b-f28c-40cf-9a1c-207d12a5350b' \
--header 'Content-Type: application/json'
Results in the following response payload from Circle:
{
"data": {
"id": "60b9ff8b-f28c-40cf-9a1c-207d12a5350b",
"amount": {
"amount": "1.67",
"currency": "USD"
},
"amountPaid": {
"amount": "0.00",
"currency": "USD"
},
"amountRefunded": {
"amount": "0.00",
"currency": "USD"
},
"settlementCurrency": "USD",
"paymentMethods": [
{
"type": "blockchain",
"chain": "ETH",
"address": "0xa7fa0314e4a3f946e9c8a5f404bb9819ed442079"
}
],
"fees": [
{
"type": "blockchainLeaseFee",
"amount": "0.00",
"currency": "USD"
}
],
"paymentIds": [],
"refundIds": [],
"timeline": [
{
"status": "pending",
"context": null,
"reason": null,
"time": "2023-03-28T12:26:42.346901Z"
},
{
"status": "created",
"context": null,
"reason": null,
"time": "2023-03-28T12:26:39.607607Z"
}
],
"expiresOn": "2023-03-28T20:26:42.238810Z",
"updateDate": "2023-03-28T12:26:42.345054Z",
"createDate": "2023-03-28T12:26:39.604637Z",
"merchantWalletId": "1013833795"
}
}
From the Circle developer logs screen, I can see a summary of all of my requests, including the response payload:
Conclusion
Readers of my publications may recall that I have been focused on the following mission statement, which I feel can apply to any IT professional:
“Focus your time on delivering features/functionality that extends the value of your intellectual property. Leverage frameworks, products, and services for everything else.”
- J. Vester
When I look back on each of my web3 publications, I am always taken aback by the number of steps or complexity involved in getting from point A to point B. While I am sure this was the case when I started working with web2, I feel like the learning cost is much higher now. This is where Circle really seems to bridge the gap.
In the example illustrated above, I was able to leverage Java and Spring Boot to integrate a RESTful API into the Circle platform and start making real-time, online, secure payments. As a result, Circle is helping me adhere to my mission statement.
Things tend to move fast in technology fields, and early adopters are often faced with challenges like:
Documentation that is not polished, accurate, or even available
Tools and technologies with steep learning curves
Inability to easily integrate with existing frameworks, services, and applications
From what I found in this exercise, Circle has avoided these pitfalls – giving me the option to avoid bank hours, processing times, and costly fees – while building my business with the USDC digital currency. In addition to USDC, it also supports card payments, cryptocurrencies, and other digital payment methods. And it has distinct advantages over other payment technologies, such as Apple Pay, PayPal, and Google Pay.
If you are interested in the source code for this publication, it can be found over at GitLab:
https://gitlab.com/johnjvester/circle-sdk-demo
Have a really great day!