- Published on
What Is an Orchestrator?
Table of Contents
- π§ What Is an Orchestrator?
- π Key Idea:
- β Example Definition
- π§± Difference Between Class, Service, and Orchestrator
- π§ Think of it Like:
- π― Why Use an Orchestrator?
- β Benefits:
- β But... Isnβt the Original CheckoutService an Orchestrator?
- π Original CheckoutService:
- β True Orchestrator:
- π‘ How to Create an Orchestrator (Step-by-Step)
- π Bonus: Orchestrator vs Aggregator
- β Summary
- π¦ Project Structure: src/
- π§© 1. domain/models/User.ts
- π₯ 2. types/UserDTO.ts
- π§ͺ 3. services/UserValidator.ts
- π 4. services/PasswordEncryptor.ts
- πΎ 5. services/UserRepository.ts
- π§ 6. services/WelcomeMailer.ts
- π― 7. orchestrators/RegisterUser.ts
- π 8. controllers/RegisterController.ts
- π 9. index.ts (Express Setup)
- β Benefits Recap
π§ What Is an Orchestrator?
An Orchestrator is a class (or module) responsible for coordinating multiple responsibilities by delegating tasks to other services or components. It does not implement logic itself β it uses others to get the job done.
π Key Idea:
An Orchestrator orchestrates the flow between components that each follow the Single Responsibility Principle (SRP).
β Example Definition
class CheckoutOrchestrator {
constructor(
private cartService: CartService,
private cartValidator: CartValidator,
private discountCalculator: DiscountCalculator,
private paymentProcessor: PaymentProcessor,
private orderPersister: OrderPersister,
private orderNotifier: OrderNotifier
) {}
async checkout(userId: string) {
const cart = await this.cartService.getCart(userId);
this.cartValidator.validate(cart);
const total = this.discountCalculator.calculate(cart);
const paid = await this.paymentProcessor.process(userId, total);
if (!paid) throw new Error("Payment failed");
await this.orderPersister.persist(cart);
await this.orderNotifier.notify(userId);
}
}
π It connects many small, specialized services β and just controls how and when to call them.
π§± Difference Between Class, Service, and Orchestrator
| Term | Responsibility | Contains Logic? | Used For |
|---|---|---|---|
| Class | Generic unit of code | Depends on the use case | Represents any abstraction or structure |
| Service | Performs a single responsibility (SRP) | β Yes | Business logic like "send email", "calculate discount" |
| Orchestrator | Coordinates multiple services | β No (logic lives in services) | Workflows, use cases like "checkout", "place order" |
π§ Think of it Like:
- Services = the musicians (each plays one instrument well)
- Orchestrator = the conductor (coordinates them to play a symphony)
π― Why Use an Orchestrator?
β Benefits:
| Benefit | Explanation |
|---|---|
| π¦ Separation of Concerns | Keeps coordination logic separate from business logic |
| π Reusability | Services can be reused elsewhere without the orchestration |
| π§ͺ Testability | You can mock services in orchestrator tests |
| π§° Scalability | Complex flows (e.g., workflows, transactions) stay clean and manageable |
| πΈ Flexibility | Easy to swap services (e.g., change PaymentProcessor) without touching flow logic |
β But... Isnβt the Original CheckoutService an Orchestrator?
Great question!
It looks like one β but it violates SRP because:
- It does orchestration + logic in the same place
- It knows too much about everything: validation, discounts, payment, persistence, communication
So it's not a true orchestrator. It's a God class, not a conductor.
π Original CheckoutService:
class CheckoutService {
// does everything: validation, discount, payment, saving, notification
}
β True Orchestrator:
class CheckoutOrchestrator {
// coordinates services that handle the above individually
}
π‘ How to Create an Orchestrator (Step-by-Step)
-
Identify the Use Case (e.g., Checkout, RegisterUser, PlaceOrder)
-
List All Steps What services/operations are required? e.g., ValidateCart β ApplyDiscount β ChargePayment β SaveOrder β SendEmail
-
Create SRP Services for Each Step
class CartValidator {}
class DiscountCalculator {}
class PaymentProcessor {}
class OrderPersister {}
class OrderNotifier {}
- Create the Orchestrator to Glue the Flow
class CheckoutOrchestrator {
constructor(private services...) {}
execute(userId: string) {
// call each service in correct order
}
}
- Test Each Service Separately + Test Orchestrator as Integration
π Bonus: Orchestrator vs Aggregator
| Concept | Role |
|---|---|
| Orchestrator | Controls the flow of execution |
| Aggregator | Gathers results from multiple services |
Sometimes an orchestrator also aggregates, but orchestration is about control flow, not data gathering.
β Summary
| Concept | Description |
|---|---|
| Class | General-purpose container of data/behavior |
| Service | Small, focused logic unit with one responsibility |
| Orchestrator | Coordinates services to fulfill a use case |
β The orchestrator uses SRP-compliant services to keep workflows clean, scalable, and testable.
Weβll use the example of a User Registration system in a backend-style architecture.
π¦ Project Structure: src/
src/
β
βββ domain/
β βββ models/
β βββ User.ts
β
βββ services/
β βββ UserValidator.ts
β βββ PasswordEncryptor.ts
β βββ UserRepository.ts
β βββ WelcomeMailer.ts
β
βββ orchestrators/
β βββ RegisterUser.ts
β
βββ controllers/
β βββ RegisterController.ts
β
βββ index.ts
βββ types/
βββ UserDTO.ts
π§© 1. domain/models/User.ts
export class User {
constructor(
public id: string,
public name: string,
public email: string,
public hashedPassword: string
) {}
}
π₯ 2. types/UserDTO.ts
export interface UserDTO {
name: string;
email: string;
password: string;
}
π§ͺ 3. services/UserValidator.ts
import { UserDTO } from "../types/UserDTO";
export class UserValidator {
validate(user: UserDTO) {
if (!user.email.includes("@")) {
throw new Error("Invalid email");
}
if (user.password.length < 6) {
throw new Error("Password must be at least 6 characters");
}
}
}
π 4. services/PasswordEncryptor.ts
import bcrypt from "bcryptjs";
export class PasswordEncryptor {
async encrypt(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
}
πΎ 5. services/UserRepository.ts
import { User } from "../domain/models/User";
import { v4 as uuid } from "uuid";
export class UserRepository {
private users: User[] = [];
async save(name: string, email: string, hashedPassword: string): Promise<User> {
const user = new User(uuid(), name, email, hashedPassword);
this.users.push(user);
return user;
}
findByEmail(email: string): User | undefined {
return this.users.find(u => u.email === email);
}
}
π§ 6. services/WelcomeMailer.ts
export class WelcomeMailer {
async send(email: string) {
console.log(`π¨ Sent welcome email to ${email}`);
}
}
π― 7. orchestrators/RegisterUser.ts
import { UserDTO } from "../types/UserDTO";
import { UserValidator } from "../services/UserValidator";
import { PasswordEncryptor } from "../services/PasswordEncryptor";
import { UserRepository } from "../services/UserRepository";
import { WelcomeMailer } from "../services/WelcomeMailer";
import { User } from "../domain/models/User";
export class RegisterUser {
constructor(
private validator: UserValidator,
private encryptor: PasswordEncryptor,
private repository: UserRepository,
private mailer: WelcomeMailer
) {}
async execute(data: UserDTO): Promise<User> {
this.validator.validate(data);
const hashed = await this.encryptor.encrypt(data.password);
const user = await this.repository.save(data.name, data.email, hashed);
await this.mailer.send(data.email);
return user;
}
}
π 8. controllers/RegisterController.ts
import { Request, Response } from "express";
import { RegisterUser } from "../orchestrators/RegisterUser";
import { UserValidator } from "../services/UserValidator";
import { PasswordEncryptor } from "../services/PasswordEncryptor";
import { UserRepository } from "../services/UserRepository";
import { WelcomeMailer } from "../services/WelcomeMailer";
export const RegisterController = async (req: Request, res: Response) => {
const validator = new UserValidator();
const encryptor = new PasswordEncryptor();
const repository = new UserRepository();
const mailer = new WelcomeMailer();
const useCase = new RegisterUser(validator, encryptor, repository, mailer);
try {
const user = await useCase.execute(req.body);
res.status(201).json({ id: user.id, email: user.email });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
};
π 9. index.ts (Express Setup)
import express from "express";
import { RegisterController } from "./controllers/RegisterController";
const app = express();
app.use(express.json());
app.post("/register", RegisterController);
app.listen(3000, () => console.log("π Server on http://localhost:3000"));
β Benefits Recap
| Feature | Outcome |
|---|---|
| SRP | Each service does one job only |
| Testable | You can unit test services and orchestrator separately |
| Scalable | Easy to add features like "Send SMS" or "Log metrics" |
| Replaceable | Swap Email service or Repository without rewriting logic |
| Separation of Concerns | Clear roles between workflow and logic |