Create a Sample Application That Supports Microservice Transactions
This tutorial describes how to create a sample application that supports microservice transactions in ScalarDB.
Overview
This tutorial illustrates the process of creating a sample e-commerce application, where items can be ordered and paid for with a line of credit through transactions with a two-phase commit interface in ScalarDB.
The sample application has two microservices called the Customer Service and the Order Service based on the database-per-service pattern:
- The Customer Service manages customer information, including line-of-credit information, credit limit, and credit total.
- The Order Service is responsible for order operations like placing an order and getting order histories.
Each service has gRPC endpoints. Clients call the endpoints, and the services call each endpoint as well.
The databases that you will be using in the sample application are Cassandra and MySQL. The Customer Service and the Order Service use Cassandra and MySQL, respectively, through ScalarDB.
As shown in the diagram, both services access a small Coordinator database used for the Consensus Commit protocol. The database is service-independent and exists for managing transaction metadata for Consensus Commit in a highly available manner.
In the sample application, for ease of setup and explanation, we co-locate the Coordinator database in the same Cassandra instance of the Order Service. Alternatively, you can manage the Coordinator database as a separate database.
Since the focus of the sample application is to demonstrate using ScalarDB, application-specific error handling, authentication processing, and similar functions are not included in the sample application. For details about exception handling in ScalarDB, see How to handle exceptions.
Additionally, for the purpose of the sample application, each service has one container so that you can avoid using request routing between the services. However, for production use, because each service typically has multiple servers or hosts for scalability and availability, you should consider request routing between the services in transactions with a two-phase commit interface. For details about request routing, see Request routing in transactions with a two-phase commit interface.
Service endpoints
The endpoints defined in the services are as follows:
-
Customer Service
getCustomerInfo
payment
prepare
validate
commit
rollback
repayment
-
Order Service
placeOrder
getOrder
getOrders
What you can do in this sample application
The sample application supports the following types of transactions:
- Get customer information through the
getCustomerInfo
endpoint of the Customer Service. - Place an order by using a line of credit through the
placeOrder
endpoint of the Order Service and thepayment
,prepare
,validate
,commit
, androllback
endpoints of the Customer Service.- Checks if the cost of the order is below the customer's credit limit.
- If the check passes, records the order history and updates the amount the customer has spent.
- Get order information by order ID through the
getOrder
endpoint of the Order Service and thegetCustomerInfo
,prepare
,validate
,commit
, androllback
endpoints of the Customer Service. - Get order information by customer ID through the
getOrders
endpoint of the Order Service and thegetCustomerInfo
,prepare
,validate
,commit
, androllback
endpoints of the Customer Service. - Make a payment through the
repayment
endpoint of the Customer Service.- Reduces the amount the customer has spent.
The getCustomerInfo
endpoint works as a participant service endpoint when receiving a transaction ID from the coordinator.
Prerequisites for this sample application
- One of the following Java Development Kits (JDKs):
- Oracle JDK 8
- OpenJDK 8 from Eclipse Temurin, Amazon Corretto, or Microsoft
- Docker 20.10 or later with Docker Compose V2 or later
This sample application only works with Java 8. However, ScalarDB itself works with Java LTS versions, which means that you can use Java LTS versions for your application that uses ScalarDB. For details on the requirements of ScalarDB, such as which Java versions can be used, see Requirements.
Set up ScalarDB
The following sections describe how to set up the sample application that supports microservices transactions in ScalarDB.
Clone the ScalarDB samples repository
Open Terminal, then clone the ScalarDB samples repository by running the following command:
git clone https://github.com/scalar-labs/scalardb-samples
Then, go to the directory that contains the sample application by running the following command:
cd scalardb-samples/microservice-transaction-sample
Start Cassandra and MySQL
Cassandra and MySQL are already configured for the sample application, as shown in database-cassandra.properties
and database-mysql.properties
, respectively. For details about configuring the multi-storage transactions feature in ScalarDB, see How to configure ScalarDB to support multi-storage transactions.
To start Cassandra and MySQL, which are included in the Docker container for the sample application, run the following command:
docker-compose up -d mysql cassandra
Starting the Docker container may take more than one minute depending on your development environment.
Load the schema
The database schema (the method in which the data will be organized) for the sample application has already been defined in customer-service-schema.json
for the Customer Service and order-service-schema.json
for the Order Service.
To apply the schema, go to the ScalarDB Releases page and download the ScalarDB Schema Loader that matches the version of ScalarDB that you want to use to the scalardb-samples/microservice-transaction-sample
folder.
MySQL
To load the schema for customer-service-schema.json
into MySQL, run the following command, replacing <VERSION>
with the version of the ScalarDB Schema Loader that you downloaded:
java -jar scalardb-schema-loader-<VERSION>.jar --config database-mysql.properties --schema-file customer-service-schema.json
Cassandra
To load the schema for order-service-schema.json
into Cassandra, run the following command, replacing <VERSION>
with the version of the ScalarDB Schema Loader that you downloaded:
java -jar scalardb-schema-loader-<VERSION>.jar --config database-cassandra.properties --schema-file order-service-schema.json --coordinator
Schema details
As shown in customer-service-schema.json
, all the tables for the Customer Service are created in the customer_service
namespace.
customer_service.customers
: a table that manages customers' informationcredit_limit
: the maximum amount of money a lender will allow each customer to spend when using a line of creditcredit_total
: the amount of money that each customer has already spent by using their line of credit
As shown in order-service-schema.json
, all the tables for the Order Service are created in the order_service
namespace.
order_service.orders
: a table that manages order informationorder_service.statements
: a table that manages order statement informationorder_service.items
: a table that manages information of items to be ordered
The Entity Relationship Diagram for the schema is as follows:
Load the initial data by starting the microservices
Before starting the microservices, build the Docker images of the sample application by running the following command:
./gradlew docker
Then, start the microservices by running the following command:
docker-compose up -d customer-service order-service
After starting the microservices and the initial data has loaded, the following records should be stored in the customer_service.customers
table:
customer_id | name | credit_limit | credit_total |
---|---|---|---|
1 | Yamada Taro | 10000 | 0 |
2 | Yamada Hanako | 10000 | 0 |
3 | Suzuki Ichiro | 10000 | 0 |
And the following records should be stored in the order_service.items
table:
item_id | name | price |
---|---|---|
1 | Apple | 1000 |
2 | Orange | 2000 |
3 | Grape | 2500 |
4 | Mango | 5000 |
5 | Melon | 3000 |
Execute transactions and retrieve data in the sample application
The following sections describe how to execute transactions and retrieve data in the sample e-commerce application.
Get customer information
Start with getting information about the customer whose ID is 1
by running the following command:
./gradlew :client:run --args="GetCustomerInfo 1"
You should see the following output:
...
{"id": 1,"name": "Yamada Taro","credit_limit": 10000}
...
At this time, credit_total
isn't shown, which means the current value of credit_total
is 0
.
Place an order
Then, have customer ID 1
place an order for three apples and two oranges by running the following command:
The order format in this command is ./gradlew run --args="PlaceOrder <CUSTOMER_ID> <ITEM_ID>:<COUNT>,<ITEM_ID>:<COUNT>,..."
.
./gradlew :client:run --args="PlaceOrder 1 1:3,2:2"
You should see a similar output as below, with a different UUID for order_id
, which confirms that the order was successful:
...
{"order_id": "4ccdb21c-ac03-4b48-bcb7-cad57eac1e79"}
...
Check order details
Check details about the order by running the following command, replacing <ORDER_ID_UUID>
with the UUID for the order_id
that was shown after running the previous command:
./gradlew :client:run --args="GetOrder <ORDER_ID_UUID>"
You should see a similar output as below, with different UUIDs for order_id
and timestamp
:
...
{"order": {"order_id": "4ccdb21c-ac03-4b48-bcb7-cad57eac1e79","timestamp": 1631605253126,"customer_id": 1,"customer_name": "Yamada Taro","statement": [{"item_id": 1,"item_name": "Apple","price": 1000,"count": 3,"total": 3000},{"item_id": 2,"item_name": "Orange","price": 2000,"count": 2,"total": 4000}],"total": 7000}}
...
Place another order
Place an order for one melon that uses the remaining amount in credit_total
for customer ID 1
by running the following command:
./gradlew :client:run --args="PlaceOrder 1 5:1"
You should see a similar output as below, with a different UUID for order_id
, which confirms that the order was successful:
...
{"order_id": "0b10db66-faa6-4323-8a7a-474e8534a7ee"}
...
Check order history
Get the history of all orders for customer ID 1
by running the following command:
./gradlew :client:run --args="GetOrders 1"