Building High-Performance e-Wallet Backend Service Using Akka-Serverless FinTech Use-Case.
Oluwaseyi Otun
Lead Backend Software Engineer ( Scala, Python, Java, Kafka, ZIO, Akka)
Cloud computing has become ideal for delivering enterprise applications and preferred solutions for companies extending their infrastructure or launching innovations. The on-demand availability of computer system resources(e.g network, servers, storage, application, and services) can be rapidly provisioned and released with minimal management effort or service provider interaction. The cloud provider offers services such as platform as a service, software as a service, and infrastructure as a service. In this article, we are doing to explore Serverless computing as a service.
What Is Serverless
Serverless computing is a method of providing backend services on an as-used basis. A serverless provider allows users to write and deploy code without the hassle of worrying about the underlying infrastructure.
What is Akka Serverless
Akka Serverless is a Platform-as-a-Service (PaaS) offering by Lightbend that aims at cloud-native application development. It is built on Lightbend's Akka Platform technology with serverless offerings. With Akka Serverless, developers don't have to worry or care about the database, the infrastructure, and the integrations, developers can focus all their time on the business logic. With Akka Serverless, developers can build a stateful serverless solution. It is polyglot i.e developers can choose the completely different language of their choice, currently, it has support for Java and Node JS.
This article will describe a fictitious FinTech startup use case. It wants to build and lunch a P2P e-wallet payment solution by using Akka-Serverless to accelerate time to market for this wallet solutions.
High-Level Overview of e-Wallet Business Model
The E-wallet business model provides a user-friendly experience where?a user can transfer funds to another user. The payments via digital wallets are usually in small amounts which get used for rent, utility bills, mobile recharge, etc. It is sometimes called Peer to Peer (P2P) transaction among users on the same wallet network. This kind of financial service encourages financial inclusion.
Use Case
Lasgidi PayBuddy?is a fictitious North American FinTech startup that provides P2P wallet payment services in Canada. They just raised Series A funding from investors. As a startup, they have adopted a cloud-first strategy to rapidly build and scale their solution to millions of users across Canada. They want to take advantage of services offered by cloud providers to optimize operations and speed up development processes. They evaluated various cloud providers and services in the market today. They have decided to go with Akka-Serverless by Lightbend to develop their various backend services. The main reason for going with Akka-Serverless is rapid development without worrying about infrastructure management or database tuning. You can scale your solution to millions of concurrent users. Akka-Serverless under the hood uses the Akka framework. A proven framework for building distributed, high-performance, and scalable solutions.
Below are the functional and non-functional requirements:
With the functional and non-functional requirements above we need to build a wallet service that can scale for millions of concurrent users.
Akka-Serverless Building Blocks
The main components of an Akka Serverless service are as follow:
Event Sourced Entities persist all the events that led to the current state of the entity instead of persisting in the current state only. It is horizontally scaled. The fundamental idea of?event sourcing?ensures that every change to the state of an application is captured in an event object and that these event objects are stored in the sequence they were applied for the same lifetime as the application state itself.
Value Entity persists state on every change. It has CRUD application model semantics.
Akka Serverless uses protocol buffers to serialize data to the underlying data store. It automatically detects if an updated state is?protobuf, and serializes it using?protobuf.
We are going to implements our e-Wallet services using an Event-Sourcing Entity.
It is an ideal design pattern for building modern FinTech backend services because it gives us the following advantages:
Building e-Wallet using Akka-Serverless
Akka-Serverless uses gRPC as a communication protocol for exposing our services to the outside world. We will define our service API and domain entity using gRPC protobuf descriptors with?Proto3. It will generate a service interface where we will implement our business logic. Akka-Serverless promotes design first approach for building services. We are going to design our gRPC protobuf descriptors that define our API services. It gives a clean interface contract and domain modeling.
领英推荐
Below is a proto buffer for our wallet domain. We define all our domains, events, and state modeling.
syntax = "proto3";
package com.bigdataConcept.akka.serverless.ewallet.domain;
import "akkaserverless/annotations.proto";
import "google/protobuf/timestamp.proto";
option java_outer_classname = "WalletDomain";
option (akkaserverless.file).event_sourced_entity = {
name: "Wallet"
entity_type: "wallet"
state: "WalletState"
events: ["WalletCreatedEvent","WalletCreditedEvent", "WalletDebitedEvent"]
};
enum TransactionType {
Credit = 0;
Debit = 1;
}
message WalletCreditedEvent{
string wallet_id = 1;
Money amt = 2;
TransactionType transType = 3;
google.protobuf.Timestamp transactionDate = 4;
}
message WalletDebitedEvent{
string wallet_id = 1;
Money amt = 2;
TransactionType transType = 3;
google.protobuf.Timestamp transactionDate = 4;
}
message WalletCreatedEvent{
string wallet_id = 1;
Profile profile = 2;
}
message WalletState {
double balance = 1;
Profile profile = 2;
repeated Transaction transactionHistory = 3;
}
message Transaction{
double amount = 1;
TransactionType transactionType = 2;
google.protobuf.Timestamp transactionDate = 3;
}
message Profile{
string name = 1;
string surname = 2;
string email = 3;
}
message Money{
double amount = 1;
string currency = 2;
}
Let break the code down.
Below is a proto buffer for our wallet API Services. We define the service interface.
syntax = "proto3";
import "google/protobuf/empty.proto";
import "akkaserverless/annotations.proto";
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "value-entities/wallet_domain.proto";
package com.bigdataConcept.akka.serverless.ewallet;
option java_outer_classname = "WalletApi";
message TransactionResponse {
string message = 1;
}
message WalletResponse {
string message = 1;
}
message DebitCommand {
string wallet_id = 1 [(akkaserverless.field).entity_key = true];
com.bigdataConcept.akka.serverless.ewallet.domain.Money money = 2;
}
message CreditCommand {
string wallet_id = 1 [(akkaserverless.field).entity_key = true];
com.bigdataConcept.akka.serverless.ewallet.domain.Money money = 2;
}
message CreateWalletCommand {
string wallet_id = 1 [(akkaserverless.field).entity_key = true];
com.bigdataConcept.akka.serverless.ewallet.domain.Profile profile = 2;
}
message RequestMiniStatement {
string wallet_id = 1 [(akkaserverless.field).entity_key = true];
}
message MiniStatement {
repeated com.bigdataConcept.akka.serverless.ewallet.domain.Transaction transactions = 1;
double balance = 2;
}
message RequestProfile {
string wallet_id = 1 [(akkaserverless.field).entity_key = true];
}
message ProfileResponse {
com.bigdataConcept.akka.serverless.ewallet.domain.Profile profile = 1;
}
service WalletService {
option (akkaserverless.service) = {
type : SERVICE_TYPE_ENTITY
component : ".domain.Wallet"
};
rpc Withdraw(DebitCommand) returns (TransactionResponse){
option (google.api.http) = {
post: "/wallet/{wallet_id}/debit",
body: "*",
};
}
rpc CreateWallet(CreateWalletCommand) returns (WalletResponse){
option (google.api.http) = {
post: "/wallet/{wallet_id}/create",
body: "*",
};
}
rpc Deposit(CreditCommand) returns (TransactionResponse){
option (google.api.http) = {
post: "/wallet/{wallet_id}/credit",
body: "*",
};
}
rpc GetMiniStatement(RequestMiniStatement) returns (MiniStatement){
option (google.api.http) = {
get: "/wallet/{wallet_id}/ministatement"
};
}
rpc GetProfile(RequestProfile) returns (ProfileResponse){
option (google.api.http) = {
get: "/wallet/{wallet_id}/profile"
};
}
}
Below are code snippets from Wallet business logic.
package com.bigdataConcept.akka.serverless.ewallet.domain;
import com.akkaserverless.javasdk.EntityId;
import com.akkaserverless.javasdk.eventsourcedentity.*;
import com.bigdataConcept.akka.serverless.ewallet.WalletApi;
import com.google.protobuf.Timestamp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
/** An event sourced entity. */
@EventSourcedEntity(entityType = "wallet")
public class WalletImpl extends WalletInterface {
private static final Logger logger = LoggerFactory.getLogger(WalletImpl.class);
@SuppressWarnings("unused")
private final String entityId;
List<WalletDomain.Transaction> transactionList;
double balance ;
WalletDomain.Profile profile;
public WalletImpl(@EntityId String entityId) {
logger.info("Calling Walling");
this.entityId = entityId;
this.transactionList = new ArrayList<WalletDomain.Transaction>();
this.balance = 0;
this.profile = WalletDomain.Profile.newBuilder().build();
}
@Override
public WalletDomain.WalletState snapshot() {
// TODO: produce state snapshot here
logger.info("Snapshotting");
return WalletDomain.WalletState.newBuilder()
.setProfile(this.profile)
.addAllTransactionHistory(this.transactionList)
.setBalance(this.balance)
.build();
}
@Override
public void handleSnapshot(WalletDomain.WalletState snapshot) {
// TODO: restore state from snapshot here
logger.info("handleSnapShot");
transactionList.clear();
this.profile = snapshot.getProfile();
this.transactionList = snapshot.getTransactionHistoryList();
this.balance = snapshot.getBalance();
}
@Override
protected WalletApi.TransactionResponse withdraw(WalletApi.DebitCommand command, CommandContext ctx) {
logger.info("Debit Wallet" + command.getMoney().getAmount());
if(command.getMoney().getAmount() > balance)
return WalletApi.TransactionResponse.newBuilder().setMessage("Insufficient Funds").build();
WalletDomain.WalletDebitedEvent walletDebitedEvent = WalletDomain.WalletDebitedEvent.newBuilder()
.setAmt(command.getMoney())
.setTransType(WalletDomain.TransactionType.Debit)
.setTransactionDate(initTransactionDate())
.setWalletId(this.entityId)
.build();
ctx.emit(walletDebitedEvent);
return WalletApi.TransactionResponse.newBuilder().setMessage("Wallet Debited Successfully").build();
}
@Override
protected WalletApi.WalletResponse createWallet(WalletApi.CreateWalletCommand command, CommandContext ctx) {
logger.info("Create Wallet" + command.getProfile());
if(command == null || command.getProfile() == null) return WalletApi.WalletResponse.newBuilder().setMessage("Profile details must not be empty").build();
WalletDomain.WalletCreatedEvent walletCreatedEvent = WalletDomain.WalletCreatedEvent.newBuilder()
.setProfile(command.getProfile())
.build();
ctx.emit(walletCreatedEvent);
return WalletApi.WalletResponse.newBuilder().setMessage("Wallet Created Successfully").build();
}
@Override
protected WalletApi.TransactionResponse deposit(WalletApi.CreditCommand command, CommandContext ctx) {
logger.info("Credit from Wallet");
WalletDomain.WalletCreditedEvent walletCreditedEvent = WalletDomain.WalletCreditedEvent.newBuilder()
.setAmt(command.getMoney())
.setTransactionDate(initTransactionDate())
.setTransType(WalletDomain.TransactionType.Credit)
.setWalletId(this.entityId)
.build();
ctx.emit(walletCreditedEvent);
return WalletApi.TransactionResponse.newBuilder().setMessage("Wallet Credited Successfully").build();
}
@Override
protected WalletApi.MiniStatement getMiniStatement(WalletApi.RequestMiniStatement command, CommandContext ctx) {
logger.info("Get MiniStatement ");
WalletApi.MiniStatement miniStatement = WalletApi.MiniStatement.newBuilder()
.setBalance(balance)
.addAllTransactions(this.transactionList)
.build();
return miniStatement;
}
@Override
protected WalletApi.ProfileResponse getProfile(WalletApi.RequestProfile command, CommandContext ctx) {
logger.info("GetProfile" + profile.getEmail());
WalletApi.ProfileResponse profileResponse = WalletApi.ProfileResponse.newBuilder()
.setProfile(this.profile)
.build();
return profileResponse;
}
@Override
public void walletCreatedEvent(WalletDomain.WalletCreatedEvent event) {
logger.info("WalletCreatedEvent" + event.getProfile().getEmail());
this.profile = event.getProfile();
}
@Override
public void walletCreditedEvent(WalletDomain.WalletCreditedEvent event) {
logger.info("Wallet Credited Event");
transactionList.add(WalletDomain.Transaction.newBuilder()
.setTransactionDate(event.getTransactionDate())
.setAmount(event.getAmt().getAmount())
.setTransactionType(event.getTransType())
.build());
balance += event.getAmt().getAmount();
}
@Override
public void walletDebitedEvent(WalletDomain.WalletDebitedEvent event) {
logger.info("Wallet Debited Event");
transactionList.add(WalletDomain.Transaction.newBuilder()
.setTransactionDate(event.getTransactionDate())
.setTransactionType(event.getTransType())
.setAmount(event.getAmt().getAmount())
.build());
balance -= event.getAmt().getAmount();
}
private com.google.protobuf.Timestamp initTransactionDate(){
return Timestamp.newBuilder()
.setSeconds(Instant.now().getEpochSecond())
.setNanos(Instant.now().getNano())
.build();
}
Let break the code down.
As you have noticed in our business logic above we can't see any data access layer code, no database connection pool, etc. Akka-Serverless abstracts away all this from us. We are not worried about the database layer, no schema, no ORM, hibernate, no database optimization, etc. Akka-Serverless does that for us that is why Akka-Serverless is database-less.
Deployment To Akka-Serverless Cloud Platform
We build and test our service locally before deploying to the Akka Serverless cloud as a prerequisite we need to set up a local docker container running on our development machine.
We need to create an Akka-Serverless cloud account before we can deploy it to the Akka-Servless cloud. we need to sign up and create an Akka-Serverless cloud account from Lightbend Akka-Serverless cloud provider.
Below is the Akka-Serverless management console dashboard.
The full source code can be downloaded on my Github repository below and step-by-step instructions on building and deploying to the Akka-Serverless cloud platform. Feel free to clone the repo modify, play around, and enhance the source code adapt it to your use case.
Takeaways
This article explores how?we can use Akka Serverless to rapidly build cloud-native applications that scale for millions of uses. ?Akka Serverless delivers these capabilities through a declarative API-first polyglot programming model allowing the developer to focus solely on the business logic. Akka Serverless solved the challenge of managing distributed states at scale which existing?stateless Serverless Services lack.?Akka Serverless eliminates the need for databases for business-critical solutions
Akka Serverless delivers business values quickly in an extremely cost-effective by making developers more productive by focusing on building cloud-native solutions without worrying about scaling, performance, database, and underlying compute infrastructure.
Read more on Akka-Serverless from the Lightbend is the company behind Akka used to build high-Performance, scalable distributed systems, and microservice.
Thank you for reading.
Oluwaseyi Otun?is a Backend Software and Data Engineer (Scala, Java, Akka, Apache Spark). Big Data and distributed system enthusiast with a special interest in functional programming, software architecture, design pattern, microservice, clean and quality code. I love learning internals working on a system and exploring what happens under the cover.
Sr. Technical Product Marketing Manager | Pragmatic Ambassador | Data Lakes & Analytics Enthusiast
3 年Incredible blog post ??
This was great to read as I return back to Lightbend, Inc. after a brief vacation. It got the brain engine going again and the creative serverless juices are flowing. Thanks for sharing and inspiring!