Building High-Performance e-Wallet Backend Service Using Akka-Serverless FinTech Use-Case.

Building High-Performance e-Wallet Backend Service Using Akka-Serverless FinTech Use-Case.

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.

No alt text provided for this image

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:

  • The system should be able to transfer funds from one wallet account to another.
  • The system should be able to serve millions of concurrent users and has the auto-scaling capability.
  • The system should be built as a cloud-native solution.
  • The system should have zero infrastructure maintenance.
  • The system should be build using an event-sourcing design pattern.

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:

  1. Stateful services encapsulate business logic in Value Entities or Event Sourced Entities.
  2. Actions are stateless functions that can be triggered by gRPC or HTTP calls.

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:

  1. Gives you the ability to determine the state of your system at any point in time using snapshots.
  2. Provides an audit log out-of-the-box that’s a natural fit for the FinTech industry’s regulations and compliance requirements.
  3. Highly scalable reads.
  4. Single source of truth.
  5. All of the history relevant to your business is preserved.
  6. The current state of our application and derived from the sequence of events.

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.

  1. Profile models wallet user information name, surname and email, mobile number.
  2. Money models amount and currency.
  3. TransType models Credit and Debit.
  4. Transaction models transaction that occurs in the wallet it can be either debit or credit transaction and transaction date rec.
  5. Walletstate models the wallet which is profile, list of transactions, and current balance.
  6. WalletCreated event is captured when creating a wallet or open a wallet account.
  7. WalletDebited event is captured when we debit the wallet balance.
  8. WalletCredited event is capture when we credit the wallet balance.

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.

  1. Create wallet command is invoked when a user wants to create a wallet account or profile and emits the WalletCreated event successfully. We change the state of the wallet to profile and initial zero balance account.
  2. Credit Wallet command is invoked deposit methods when we want to deposit money into the wallet we validate the command and emits WalletCredited event and increase the balance with the amount
  3. Debit Wallet command invoked withdraw method and decrease the balance by the amount and emit WalletDebited event
  4. We called handle snapshot to
  5. Minit statement method is called to fetch the transaction history. The history is created from the State of the entity memory from snapshots
  6. GetProfile method API retrieves the user's profile from in SnapShots with is an in-memory state of the entity.

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.

No alt text provided for this image

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.

Leon Stigter

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!

要查看或添加评论,请登录

Oluwaseyi Otun的更多文章

社区洞察

其他会员也浏览了