Service Composition
A service composition is an aggregate of services collectively composed to automate a particular task or business process.
This guide walks you through the process of implementing a service composition using Ballerina language.
The following are the sections available in this guide.
What you’ll build
To understanding how you can build a service composition using Ballerina, let's consider a real-world use case of a Travel agency that arranges complete tours for users. A tour package includes airline ticket reservation, hotel room reservation and car rental. Therefore, the Travel agency service requires communicating with other necessary back-ends. The following diagram illustrates this use case clearly.
Travel agency is the service that acts as the composition initiator. The other three services are external services that the travel agency service calls to do airline ticket booking, hotel reservation and car rental. These are not necessarily Ballerina services and can theoretically be third-party services that the travel agency service calls to get things done. However, for the purposes of setting up this scenario and illustrating it in this guide, these third-party services are also written in Ballerina.
Prerequisites
- JDK 1.8 or later
- Ballerina Distribution
- A Text Editor or an IDE
Optional requirements
- Ballerina IDE plugins (IntelliJ IDEA, VSCode, Atom)
- Docker
Developing the service
Before you begin
Understand the package structure
Ballerina is a complete programming language that can have any custom project structure that you wish. Although the language allows you to have any package structure, use the following package structure for this project to follow this guide.
service-composition
└── src
├── AirlineReservation
│ ├── airline_reservation_service.bal
│ └── test
│ └── airline_reservation_service_test.bal
├── CarRental
│ ├── car_rental_service.bal
│ └── test
│ └── car_rental_service_test.bal
├── HotelReservation
│ ├── hotel_reservation_service.bal
│ └── test
│ └── hotel_reservation_service_test.bal
└── TravelAgency
├── test
│ └── travel_agency_service_test.bal
└── travel_agency_service.bal
Package AirlineReservation
contains the service that provides online flight ticket reservations.
Package CarRental
contains the service that provides online car rentals.
Package HotelReservation
contains the service that provides online hotel room reservations.
The travel_agency_service.bal
file provides travel agency service, which consumes the other three services, and arranges a complete tour for the requested user.
Implementation
Let's look at the implementation of the travel agency service, which acts as the composition initiator.
Arranging a complete tour travel agency service requires communicating with three other services: airline reservation, hotel reservation, and car rental. All these services accept POST requests with appropriate JSON payloads and send responses back with JSON payloads. Request and response payloads are similar for all three backend services.
Sample request payload:
{"Name":"Bob", "ArrivalDate":"12-03-2018", "DepartureDate":"13-04-2018", "Preference":<service_dependent_preference>};
Sample response payload:
{"Status":"Success"}
When a client initiates a request to arrange a tour, the travel agency service first needs to communicate with the airline reservation service to book a flight ticket. To check the implementation of airline reservation service, see the airline_reservation_service.bal file.
Once the airline ticket reservation is successful, the travel agency service needs to communicate with the hotel reservation service to reserve hotel rooms. To check the implementation of hotel reservation service, see the hotel_reservation_service.bal file.
Finally, the travel agency service needs to connect with the car rental service to arrange internal transports. To check the implementation of car rental service, see the car_rental_service.bal file.
If all services work successfully, the travel agency service confirms and arrange the complete tour for the user. The skeleton of travel_agency_service.bal
file is attached below. Inline comments are added for better understanding.
Refer to the travel_agency_service.bal to see the complete implementation of the travel agency service.
travel_agency_service.bal
package TravelAgency;
import ballerina/net.http;
// Service endpoint
endpoint http:ServiceEndpoint travelAgencyEP {
port:9090
};
// Client endpoint to communicate with Airline reservation service
endpoint http:ClientEndpoint airlineReservationEP {
targets:[{uri:"http://localhost:9091/airline"}]
};
// Client endpoint to communicate with Hotel reservation service
endpoint http:ClientEndpoint hotelReservationEP {
targets:[{uri:"http://localhost:9092/hotel"}]
};
// Client endpoint to communicate with Car rental service
endpoint http:ClientEndpoint carRentalEP {
targets:[{uri:"http://localhost:9093/car"}]
};
// Travel agency service to arrange a complete tour for a user
@http:ServiceConfig {basePath:"/travel"}
service<http:Service> travelAgencyService bind travelAgencyEP {
// Resource to arrange a tour
@http:ResourceConfig {methods:["POST"], consumes:["application/json"], produces:["application/json"]}
arrangeTour (endpoint client, http:Request inRequest) {
http:Response outResponse = {};
// JSON payload format for an HTTP OUT request
json outReqPayload = {"Name":"", "ArrivalDate":"", "DepartureDate":"", "Preference":""};
// Try parsing the JSON payload from the user request
// If payload parsing fails, send a "Bad Request" message as the response
// Reserve airline ticket for the user by calling the airline reservation service
// Reserve hotel room for the user by calling the hotel reservation service
// Renting car for the user by calling the car rental service
// If all three services response positive status, send a successful message to the user
outResponse.setJsonPayload({"Message":"Congratulations! Your journey is ready!!"});
_ = client -> respond(outResponse);
}
}
Let's now look at the code segment that is responsible for communicating with the airline reservation service.
// Reserve airline ticket for the user by calling Airline reservation service
http:Request outReqAirline = {};
http:Response inResAirline = {};
// construct the payload
json outReqPayloadAirline = outReqPayload;
outReqPayloadAirline.Preference = airlinePreference;
outReqAirline.setJsonPayload(outReqPayloadAirline);
// Send a post request to airlineReservationService with appropriate payload and get response
inResAirline =? airlineReservationEP -> post("/reserve", outReqAirline);
// Get the reservation status
var airlineResPayload =? inResAirline.getJsonPayload();
string airlineReservationStatus = airlineResPayload.Status.toString();
// If reservation status is negative, send a failure response to user
if (airlineReservationStatus.equalsIgnoreCase("Failed")) {
outResponse.setJsonPayload({"Message":"Failed to reserve airline! " +
"Provide a valid 'Preference' for 'Airline' and try again"});
_ = client -> respond(outResponse);
return;
}
The above code shows how the travel agency service initiates a request to the airline reservation service to book a flight ticket. airlineReservationEP
is the client endpoint you defined through which the Ballerina service communicates with the external airline reservation service.
Let's now look at the code segment that is responsible for communicating with the hotel reservation service.
// Reserve hotel room for the user by calling Hotel reservation service
http:Request outReqHotel = {};
http:Response inResHotel = {};
// construct the payload
json outReqPayloadHotel = outReqPayload;
outReqPayloadHotel.Preference = hotelPreference;
outReqHotel.setJsonPayload(outReqPayloadHotel);
// Send a post request to hotelReservationService with appropriate payload and get response
inResHotel =? hotelReservationEP -> post("/reserve", outReqHotel);
// Get the reservation status
var hotelResPayload =? inResHotel.getJsonPayload();
string hotelReservationStatus = hotelResPayload.Status.toString();
// If reservation status is negative, send a failure response to user
if (hotelReservationStatus.equalsIgnoreCase("Failed")) {
outResponse.setJsonPayload({"Message":"Failed to reserve hotel! " +
"Provide a valid 'Preference' for 'Accommodation' and try again"});
_ = client -> respond(outResponse);
return;
}
The travel agency service communicates with the hotel reservation service to book a room for the client as shown above. The client endpoint defined for this external service call is hotelReservationEP
.
Finally, let's look at the code segment that is responsible for communicating with the car rental service.
// Renting car for the user by calling Car rental service
http:Request outReqCar = {};
http:Response inResCar = {};
// construct the payload
json outReqPayloadCar = outReqPayload;
outReqPayloadCar.Preference = carPreference;
outReqCar.setJsonPayload(outReqPayloadCar);
// Send a post request to carRentalService with appropriate payload and get response
inResCar =? carRentalEP -> post("/rent", outReqCar);
// Get the rental status
var carResPayload =? inResCar.getJsonPayload();
string carRentalStatus = carResPayload.Status.toString();
// If rental status is negative, send a failure response to user
if (carRentalStatus.equalsIgnoreCase("Failed")) {
outResponse.setJsonPayload({"Message":"Failed to rent car! " +
"Provide a valid 'Preference' for 'Car' and try again"});
_ = client -> respond(outResponse);
return;
}
As shown above, the travel agency service rents a car for the requested user by calling the car rental service. carRentalEP
is the client endpoint defined to communicate with the external car rental service.
Testing
Try it out
- Start all four HTTP services by entering the following commands in separate terminals. This will start the
Airline Reservation
,Hotel Reservation
,Car Rental
andTravel Agency
services in ports 9091, 9092, 9093 and 9090 respectively.
bash
<SAMPLE_ROOT_DIRECTORY>/src$ ballerina run AirlineReservation/
bash
<SAMPLE_ROOT_DIRECTORY>/src$ ballerina run HotelReservation/
bash
<SAMPLE_ROOT_DIRECTORY>/src$ ballerina run CarRental/
bash
<SAMPLE_ROOT_DIRECTORY>/src$ ballerina run TravelAgency/
- Invoke the
travelAgencyService
by sending a POST request to arrange a tour.
bash
curl -v -X POST -d \
'{"Name":"Bob", "ArrivalDate":"12-03-2018", "DepartureDate":"13-04-2018",
"Preference":{"Airline":"Business", "Accommodation":"Air Conditioned", "Car":"Air Conditioned"}}' \
"http://localhost:9090/travel/arrangeTour" -H "Content-Type:application/json"
The `travelAgencyService` sends a response similar to the following:
```bash
< HTTP/1.1 200 OK
{"Message":"Congratulations! Your journey is ready!!"}
```
Writing unit tests
In Ballerina, the unit test cases should be in the same package inside a folder named as 'test'. When writing the test functions the below convention should be followed.
* Test functions should be annotated with @test:Config
. See the below example.
ballerina
@test:Config
function testTravelAgencyService () {
This guide contains unit test cases for each service implemented above.
To run the unit tests, go to the sample src directory and run the following command
bash
<SAMPLE_ROOT_DIRECTORY>/src$ ballerina test
To check the implementations of these test files, refer to the airline_reservation_service_test.bal, hotel_reservation_service_test.bal, car_rental_service_test.bal and travel_agency_service_test.bal.
Deployment
Once you are done with the development, you can deploy the services using any of the methods that are listed below.
Deploying locally
You can deploy the services that you developed above in your local environment. You can create the Ballerina executable archives (.balx) first and then run them in your local environment as follows.
Building
bash
<SAMPLE_ROOT_DIRECTORY>/src$ ballerina build <Package_Name>
Running
bash
<SAMPLE_ROOT_DIRECTORY>/src$ ballerina run target/<Exec_Archive_File_Name>
Deploying on Docker
You can run the services that we developed above as a docker container. As Ballerina platform offers native support for running ballerina programs on containers, you just need to put the corresponding docker annotations on your service code. Let's see how we can deploy the travel_agency_service we developed above on docker. When invoking this service make sure that the other three services (airline_reservation, hotel_reservation, and car_rental) are also up and running.
- In our travel_agency_service, we need to import
import ballerinax/docker;
and use the annotation@docker:Config
as shown below to enable docker image generation during the build time.
travel_agency_service.bal
package TravelAgency;
import ballerina/http;
import ballerinax/docker;
@docker:Config {
registry:"ballerina.guides.io",
name:"travel_agency_service",
tag:"v1.0"
}
endpoint http:ServiceEndpoint travelAgencyEP {
port:9090
};
@http:ServiceConfig {basePath:"/travel"}
service<http:Service> travelAgencyService bind travelAgencyEP {
- Now you can build a Ballerina executable archive (.balx) of the service that we developed above, using the following command. It points to the service file that we developed above and it will create an executable binary out of that.
This will also create the corresponding docker image using the docker annotations that you have configured above. Navigate to the
<SAMPLE_ROOT>/src/
folder and run the following command.
``` $ballerina build TravelAgency
Run following command to start docker container:
docker run -d -p 9090:9090 ballerina.guides.io/travel_agency_service:v1.0
`
- Once you successfully build the docker image, you can run it with the
docker run`` command that is shown in the previous step.
```
docker run -d -p 9090:9090 ballerina.guides.io/travel_agency_service:v1.0
```
Here we run the docker image with flag`` -p <host_port>:<container_port>`` so that we use the host port 9090 and the container port 9090. Therefore you can access the service through the host port.
- Verify docker container is running with the use of
$ docker ps
. The status of the docker container should be shown as 'Up'. -
You can access the service using the same curl commands that we've used above.
curl -v -X POST -d \ '{"Name":"Bob", "ArrivalDate":"12-03-2018", "DepartureDate":"13-04-2018", "Preference":{"Airline":"Business", "Accommodation":"Air Conditioned", "Car":"Air Conditioned"}}' \ "http://localhost:9090/travel/arrangeTour" -H "Content-Type:application/json"
Deploying on Kubernetes
-
You can run the services that we developed above, on Kubernetes. The Ballerina language offers native support for running a ballerina programs on Kubernetes, with the use of Kubernetes annotations that you can include as part of your service code. Also, it will take care of the creation of the docker images. So you don't need to explicitly create docker images prior to deploying it on Kubernetes.
Let's see how we can deploy the travel_agency_service we developed above on kubernetes. When invoking this service make sure that the other three services (airline_reservation, hotel_reservation, and car_rental) are also up and running. -
We need to import
import ballerinax/kubernetes;
and use@kubernetes
annotations as shown below to enable kubernetes deployment for the service we developed above.
travel_agency_service.bal
package TravelAgency;
import ballerina/http;
import ballerinax/kubernetes;
@kubernetes:Ingress {
hostname:"ballerina.guides.io",
name:"ballerina-guides-travel-agency-service",
path:"/"
}
@kubernetes:Service {
serviceType:"NodePort",
name:"ballerina-guides-travel-agency-service"
}
@kubernetes:Deployment {
image:"ballerina.guides.io/travel_agency_service:v1.0",
name:"ballerina-guides-travel-agency-service"
}
endpoint http:ServiceEndpoint travelAgencyEP {
port:9090
};
// Http client endpoint definitions
@http:ServiceConfig {basePath:"/travel"}
service<http:Service> travelAgencyService bind travelAgencyEP {
- Here we have used
@kubernetes:Deployment
to specify the docker image name which will be created as part of building this service. - We have also specified
@kubernetes:Service {}
so that it will create a Kubernetes service which will expose the Ballerina service that is running on a Pod. -
In addition we have used
@kubernetes:Ingress
which is the external interface to access your service (with path/
and host nameballerina.guides.io
) -
Now you can build a Ballerina executable archive (.balx) of the service that we developed above, using the following command. It points to the service file that we developed above and it will create an executable binary out of that. This will also create the corresponding docker image and the Kubernetes artifacts using the Kubernetes annotations that you have configured above.
``` $ballerina build TravelAgency
Run following command to deploy kubernetes artifacts:
kubectl apply -f ./target/TravelAgency/kubernetes
```
- You can verify that the docker image that we specified in
@kubernetes:Deployment
is created, by usingdocker ps images
. - Also the Kubernetes artifacts related our service, will be generated in
./target/TravelAgency/kubernetes
. - Now you can create the Kubernetes deployment using:
$ kubectl apply -f ./target/TravelAgency/kubernetes
deployment.extensions "ballerina-guides-travel-agency-service" created
ingress.extensions "ballerina-guides-travel-agency-service" created
service "ballerina-guides-travel-agency-service" created
- You can verify Kubernetes deployment, service and ingress are running properly, by using following Kubernetes commands.
$kubectl get service
$kubectl get deploy
$kubectl get pods
$kubectl get ingress
- If everything is successfully deployed, you can invoke the service either via Node port or ingress.
Node Port:
curl -v -X POST -d \
'{"Name":"Bob", "ArrivalDate":"12-03-2018", "DepartureDate":"13-04-2018",
"Preference":{"Airline":"Business", "Accommodation":"Air Conditioned", "Car":"Air Conditioned"}}' \
"http://<Minikube_host_IP>:<Node_Port>/travel/arrangeTour" -H "Content-Type:application/json"
Ingress:
Add /etc/hosts
entry to match hostname.
127.0.0.1 ballerina.guides.io
Access the service
curl -v -X POST -d \
'{"Name":"Bob", "ArrivalDate":"12-03-2018", "DepartureDate":"13-04-2018",
"Preference":{"Airline":"Business", "Accommodation":"Air Conditioned", "Car":"Air Conditioned"}}' \
"http://ballerina.guides.io/travel/arrangeTour" -H "Content-Type:application/json"
Observability
Logging
(Work in progress)
Metrics
(Work in progress)
Tracing
(Work in progress)