(Spring) Booting Java to Accept Digital Payments with USDC

(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!