Microservices architecture has changed the way software applications are developed. When there are many microservices, it becomes a challenge and sometimes a nightmare to coordinate different microservices to complete a business workflow. Developers were forced to write code for handling retries, failures, and alternate flows. This means the business logic code is mixed with system-level code to handle cross-cutting concerns. This has resulted in very large and messy code. I have seen such code a lot, and honestly, it is difficult to maintain. This is where microservices orchestration is helpful, Orchestration ensures that various microservices work together efficiently by handling service discovery, fault tolerance, load balancing, and other coordination activities.
What is Microservices Orchestration?
Microservices orchestration is a technique that performs the automated management and coordination of multiple micorservices within an application. It need not always be within the same application but can coordinate services within different applications as well. Hence, orchestration is very much a necessity to complete complex business workflows.
Orchestration typically involves scheduling, monitoring, and ensuring the execution of services in the correct order as configured in a workflow while managing failures, retries, alternate flows, and dependencies.
Orchestration is often confused with choreography, where services interact without a centralized control. In choreography, the different services interact based on the events or messages they receive. For instance, consider an e-commerce application – the OrderService creates the order and sends a message to an event broker, and that is the end of the story as far as OrderService is concerned. It does know whether the FullfilmentService picks up the message and schedules the fullfilment operations. If the event is not received or is lost, the system will be in a partial state. This lack of coordination is the major disadvantage of choreography. Hence it is better to opt for an orchestration-based approach when the business flow is complex.
Key Components of Microservices Orchestration
- Service Coordination – Ensures that microservices interact in the predefined order based on business logic
- Workflow Automation – Automates business workflows across different services.
- Failure Handling & Recovery – Ability to handle failures, perform retries, trigger alternate flows, and notify the status of the workflow
- Monitoring & Logging – Provides current state and historical state of the service invocations, information about sucess / failure, no of retries, timings and log data.
Popular Microservices Orchestration Tools
Several tools and platforms facilitate microservices orchestration, including:
1. Kubernetes
- Manages containerized microservices
- Supports auto-scaling, self-healing, and load balancing
- Provides networking and service discovery
2. Apache Airflow
- Ideal for workflow automation
- Defines workflows using Directed Acyclic Graphs (DAGs)
- Enables scheduling and monitoring of tasks
- Visual editor for defining workflow activities
- At times, it did not perform well when the data involved is large and the transformations are complex.
3. Camunda
- Focuses on business process orchestration
- Implements Business Process Model and Notation (BPMN)
- Supports event-driven workflows
4. Netflix Conductor
- Designed for microservices workflows
- Supports distributed transaction management
- Handles long-running processes efficiently
- Provides high scalability
5. Temporal
- Provides durable, fault-tolerant workflow orchestration
- Supports stateful workflows with automatic retries
- Enables microservices coordination through code-based workflows
- Supports system written in different programming languages
- Code-based approach to define workflows
- Supports dynamic workflows
- Highly scalable and distributed
Benefits of Microservices Orchestration
- Improved Efficiency – Automates service coordination, reducing manual effort. No need to write plumbing code. Developers can focus on business logic instead of writing complex orchestration code.
- Scalability – It can scale well, and most of the orchestration tools offer distributed processing capabilities to handle large loads.
- Enhanced Reliability – Provides failover, recovery, retries.
- Better Observability – Provides a snapshot of the current state of services in the workflow, status of the service invocations, performance, and logs.
- Consistent Workflows – Ensures business processes are executed in the defined order.
Challenges and Best Practices
Challenges:
- Complexity – Orchestration introduces additional components that need to be deployed, monitored, and fine-tuned. Workflows need to be created for the coordination of services.
- Single Point of Failure – A centralized orchestrator can become a bottleneck and can result in the business flow coming to a halt if the orchestrator goes down. This can be mitigated by using orchestrators that support distributed deployments across multiple machines. A standby system can also be maintained that syncs the data and is ready to take over if the primary goes down.
Best Practices:
- Use Asynchronous Communication – By design, the orchestrators use asynchronous modes of communication.
- Monitor and Log Services – Centralized monitoring and debugging capabilities to observe the current state of the workflow
Video
Examples
Here’s an example using Temporal to orchestrate two microservices:
• Order Service: Handles customer orders.
• Payment Service: Processes customer payments.
Temporal ensures reliable execution by handling retries, failures, and state management. The state of the workflow is persisted in a database, and hence, the steps of a workflow can be retriggered even in the case of large failures.
This Temporal workflow orchestrates an order processing system by executing two activities: process_payment and confirm_order. Temporal relies on a worker process that is usually defined in the application that exposes the services. You should start the Worker alongside the application. Worker listens to a task queue and pulls the tasks, executes the code in response to the task. The worker listens for workflow execution requests and runs these tasks sequentially.
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.client.WorkflowClient;
import io.temporal.serviceclient.WorkflowServiceStubs;
public class WorkerStartUp {
public static void main(String[] args) {
WorkflowServiceStubs service = WorkflowServiceStubs
.newLocalServiceStubs();
WorkflowClient client = WorkflowClient.newInstance(service);
WorkerFactory factory = WorkerFactory.newInstance(client);
Worker yourWorker = factory.newWorker("your_task_queue");
factory.start();
}
}
Code language: JavaScript (javascript)
Java Code: ( Also include the worker )
import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityMethod;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
import io.temporal.workflow.Workflow;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import io.temporal.serviceclient.WorkflowServiceStubs;
// Define Activities
@ActivityInterface
public interface OrderActivities {
@ActivityMethod
String processPayment(String orderId, double amount);
@ActivityMethod
String confirmOrder(String orderId);
}
public class OrderActivitiesImpl implements OrderActivities {
@Override
public String processPayment(String orderId, double amount) {
System.out.println("Processing payment for Order " + orderId + " of amount $" + amount);
return "Payment successful for Order " + orderId;
}
@Override
public String confirmOrder(String orderId) {
System.out.println("Confirming Order " + orderId);
return "Order " + orderId + " confirmed";
}
}
// Define Workflow
@WorkflowInterface
public interface OrderWorkflow {
@WorkflowMethod
String run(String orderId, double amount);
}
public class OrderWorkflowImpl implements OrderWorkflow {
private final OrderActivities activities = Workflow.newActivityStub(OrderActivities.class);
@Override
public String run(String orderId, double amount) {
String paymentResult = activities.processPayment(orderId, amount);
String orderResult = activities.confirmOrder(orderId);
return "Workflow complete: " + paymentResult + ", " + orderResult;
}
}
// Start the Worker
public class Main {
public static void main(String[] args) {
WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
WorkflowClient client = WorkflowClient.newInstance(service);
WorkerFactory factory = WorkerFactory.newInstance(client);
Worker worker = factory.newWorker("order-task-queue");
worker.registerWorkflowImplementationTypes(OrderWorkflowImpl.class);
worker.registerActivitiesImplementations(new OrderActivitiesImpl());
factory.start();
}
}
Code language: JavaScript (javascript)
If you are using Python, the code will be as follows
from temporalio.worker import Worker
from temporalio.client import Client
from temporalio.workflow import workflow_method, Workflow
from temporalio.activity import activity_defn
import asyncio
# Define Activities
@activity_defn
async def process_payment(order_id: str, amount: float):
print(f"Processing payment for Order {order_id} of amount ${amount}")
return f"Payment successful for Order {order_id}"
@activity_defn
async def confirm_order(order_id: str):
print(f"Confirming Order {order_id}")
return f"Order {order_id} confirmed"
# Define Workflow
class OrderWorkflow(Workflow):
@workflow_method
async def run(self, order_id: str, amount: float):
payment_result = await self.execute_activity(
process_payment, order_id, amount, start_to_close_timeout=10
)
order_result = await self.execute_activity(
confirm_order, order_id, start_to_close_timeout=5
)
return f"Workflow complete: {payment_result}, {order_result}"
# Start the Worker
async def main():
client = await Client.connect("localhost:7233")
worker = Worker(
client,
task_queue="order-task-queue",
workflows=[OrderWorkflow],
activities=[process_payment, confirm_order],
)
await worker.run()
if __name__ == "__main__":
asyncio.run(main())
Code language: CSS (css)
Conclusion
Microservices orchestration plays a crucial role in managing distributed services. It serves as the defacto tool in orchestrating business workflows in a consistent, reliable manner. Use an orchestration framework/tool if building complex business processes/workflows.
Comments
One response to “Microservices Orchestration: A Step-by-Step Guide for Scalable Workflows with Temporal”
[…] Microservices Orchestration: A Step-by-Step Guide for Scalable Workflows with Temporal […]