Events vs Commands vs Queries
ভিন্ন semantics-সহ তিন ধরনের আলাদা message — এই পার্থক্য বোঝাটাই ঠিক করে দেয় আপনি প্রতিটি integration কীভাবে design করবেন।
বাস্তব জীবনের উদাহরণ
একজন সহকর্মীকে কিছু জানানোর তিনটা আলাদা উপায়: “রিপোর্টটা পাঠিয়ে দিন” হলো একটা command — নির্দিষ্ট কারও দিকে লক্ষ্য করা, কাজের প্রত্যাশা রাখে। “রিপোর্টটা পাঠানো হয়েছে” হলো একটা event — একটা ঘটে যাওয়া তথ্য, যাকে যার দরকার তার কাছে broadcast করা। “আপনি কি রিপোর্টটা পাঠিয়েছেন?” হলো একটা query — তথ্যসহ একটা উত্তর প্রত্যাশা করে। কোডে এগুলো গুলিয়ে ফেললে ঠিক ততটাই বিভ্রান্তি তৈরি হয় যতটা কথোপকথনে হতো।
গল্পে বুঝি
ফাতিমার রেস্টুরেন্টের ফ্লোরে দুপুরের ভিড়। ওয়েটার সিনা একটা অর্ডার টিকিট রান্নাঘরে ধরিয়ে দিলেন — “একটা বিরিয়ানি বানাও।” এটা নির্দিষ্ট একজনের কাজ; রাঁধুনি খোয়ারিজমি এটা রান্না করবেন। চাল ফুরিয়ে গেলে তিনি টিকিটটা ফেরতও পাঠাতে পারেন — “আজ আর বিরিয়ানি হবে না।” মানে এই নির্দেশটা প্রত্যাখ্যানও হতে পারে।
কিছুক্ষণ পর কেউ একজন হাঁক দিলেন — “টেবিল ৫ খাওয়া শেষ করেছে!” এটা কাউকে নির্দিষ্ট করে বলা হয়নি, শুধু একটা ঘটে যাওয়া তথ্য ঘোষণা করা হলো। আর একই হাঁক শুনে কয়েকজন নিজের মতো করে সাড়া দিলেন — সিনা গিয়ে টেবিল পরিষ্কার করলেন, ক্যাশিয়ার বিল তৈরি করলেন, আর হোস্ট পরের অতিথিকে বসতে দিলেন। এদিকে ম্যানেজার শুধু জানতে চাইলেন — “আর কয়টা বিরিয়ানি বাকি আছে?” এই প্রশ্নে কিছুই বদলায় না, শুধু একটা তথ্য জানা হয়।
এখানেই তিনটা জিনিস আলাদা হয়ে যায়। অর্ডার টিকিট “বিরিয়ানি বানাও” হলো একটা command — নির্দিষ্ট একজন handler-এর দিকে লক্ষ্য করা একটা নির্দেশ, যা প্রয়োজনে প্রত্যাখ্যানও হতে পারে। “টেবিল ৫ খাওয়া শেষ করেছে!” হলো একটা event — অতীতে ঘটে যাওয়া একটা fact, যাতে অনেকজন স্বাধীনভাবে সাড়া দেয়, আর ঘোষণাকারী জানেও না কে কীভাবে react করবে। আর “আর কয়টা বাকি?” হলো একটা query — কেবল পড়া, কোনো কিছু পাল্টায় না। বাস্তবে event-driven সিস্টেমে ঠিক এই তিনটা message-এর semantics আলাদা রাখাটাই ঠিক করে দেয় আপনার service-গুলো কতটা coupled থাকবে আর failure-এ কী হবে।
তিন ধরনের Message
Commands: কোনো service-কে কিছু করতে বলে। নির্দিষ্ট একজন প্রাপকের দিকে লক্ষ্য করা। প্রেরক জানতে চায় এটা সফল হলো কিনা।
// Command: imperative verb, directed, expects handling
interface SendPasswordResetEmail {
type: 'SendPasswordResetEmail';
userId: string;
email: string;
resetToken: string;
}
interface ProcessPayment {
type: 'ProcessPayment';
orderId: string;
amount: number;
customerId: string;
} Events: কিছু একটা ঘটেছে তা রেকর্ড করে। আগ্রহী যেকোনো পক্ষের কাছে broadcast করা হয়। প্রেরক জানে না বা পরোয়াও করে না কে এটা handle করবে।
// Event: past tense, records a fact, no specific recipient
interface UserRegistered {
type: 'UserRegistered';
userId: string;
email: string;
plan: string;
registeredAt: string; // ISO 8601
}
interface OrderPlaced {
type: 'OrderPlaced';
orderId: string;
userId: string;
totalAmount: number;
items: OrderItem[];
placedAt: string;
} Queries: তথ্য চায়। একটা response প্রত্যাশা করে। সাধারণত synchronous (request/response)।
// Query: asks a question, expects an answer
interface GetUserById {
type: 'GetUserById';
userId: string;
}
interface GetOrderHistory {
type: 'GetOrderHistory';
userId: string;
fromDate: string;
limit: number;
} পার্থক্যটা কেন গুরুত্বপূর্ণ
পার্থক্যটা শুধু naming convention নয় — এটা আপনার সিস্টেমের coupling, failure mode আর semantics পাল্টে দেয়।
Commands coupling তৈরি করে:
Service A → sends command → Service B Service A, Service B সম্পর্কে জানে। B যদি down থাকে, command fail করবে। B-এর interface পাল্টালে A ভেঙে যাবে।
Events decouple করে:
Service A → emits event → Event Bus
↓
Service B (subscribes)
Service C (subscribes)
Service D (subscribes) Service A, B, C বা D সম্পর্কে কিছুই জানে না। A-কে না ছুঁয়েই নতুন subscriber যোগ করা যায়। B যদি down থাকে, event টা queue-তে অপেক্ষা করে; B ফিরে এলে সেটা process করে।
অপারেশনাল ফলাফল:
| Command | Event | |
|---|---|---|
| Coupling | টাইট — প্রেরক প্রাপককে জানে | লুজ — প্রেরক শুধু event-টাকে জানে |
| Failure | Synchronous — দুজনই একসাথে fail করে | Asynchronous — প্রেরক অপ্রভাবিত থাকে |
| Recipients | একজন | অনেকজন |
| Expectation | সফল হতেই হবে | Fire and forget |
| Naming | Imperative verb | Past tense |
Event Naming Convention
Events হলো তথ্য — সেভাবেই এদের নাম দিন:
// WRONG — sounds like a command, ambiguous
'ProcessOrder';
'UserUpdate';
'PaymentDone';
// RIGHT — past tense, specific, unambiguous
'OrderPlaced';
'UserEmailChanged';
'PaymentSucceeded';
'PaymentFailed';
'SubscriptionRenewed';
'InventoryDepleted'; একটা সহজ নিয়ম: যদি past tense ব্যবহার করতে না পারেন, তাহলে এটা সম্ভবত একটা command, event নয়।
Event Envelope
প্রতিটি event-কে metadata-সহ একটা standard envelope-এ মুড়ে দিন:
interface EventEnvelope<T = unknown> {
// Routing and identification
id: string; // unique event ID (for deduplication)
type: string; // event type name
version: number; // schema version (for evolution)
// Context
correlationId: string; // request that triggered this event (for tracing)
causationId: string; // event that caused this event (for event chains)
source: string; // service that emitted this event
// Timing
timestamp: string; // ISO 8601 UTC
// Payload
data: T;
}
// Example
const event: EventEnvelope<UserRegistered> = {
id: crypto.randomUUID(),
type: 'UserRegistered',
version: 1,
correlationId: 'req_abc123', // from the HTTP request that created the user
causationId: '', // no parent event — triggered by user action
source: 'user-service',
timestamp: new Date().toISOString(),
data: {
type: 'UserRegistered',
userId: 'u_xyz',
email: 'user@example.com',
plan: 'starter',
registeredAt: new Date().toISOString()
}
}; Envelope-টা যেকোনো consumer-কে বুঝতে দেয় একটা event কোথা থেকে এলো, কখন ঘটল, আর অন্য event-দের সাথে সম্পর্ক কী — payload parse না করেই।
CQRS: Read আর Write আলাদা করা
Command Query Responsibility Segregation ডেটা লেখার model (command side) আর ডেটা পড়ার model (query side) আলাদা করে। Events দুটোর মধ্যে সেতু হয়।
// Command side: handles writes, emits events
class OrderService {
async placeOrder(command: PlaceOrderCommand): Promise<void> {
// Validate and persist
const order = await this.db.orders.create({
userId: command.userId,
items: command.items,
status: 'placed'
});
// Emit event — read side will update its own model
await this.eventBus.publish({
type: 'OrderPlaced',
data: {
orderId: order.id,
userId: order.userId,
items: order.items,
totalAmount: order.totalAmount,
placedAt: order.createdAt
}
});
}
}
// Query side: handles reads from a denormalized read model
class OrderQueryService {
// Read model is updated by consuming 'OrderPlaced' events
// Optimized for query patterns — might be in a different database
async getOrderHistory(userId: string): Promise<OrderSummary[]> {
return this.readDb.orderSummaries.findAll({ userId });
}
}
// Event handler: keeps read model in sync
class OrderReadModelUpdater {
async handleOrderPlaced(event: EventEnvelope<OrderPlaced>): Promise<void> {
await this.readDb.orderSummaries.upsert({
id: event.data.orderId,
userId: event.data.userId,
itemCount: event.data.items.length,
totalAmount: event.data.totalAmount,
status: 'placed',
placedAt: event.data.placedAt
});
}
} CQRS সবসময় দরকার হয় না — সাধারণ একটা CRUD app-এ এটা যোগ করবেন না। এটা তখনই কাজে দেয় যখন read আর write প্যাটার্ন সত্যিই আলাদা হয় (জটিল filtering-সহ বিশাল read volume, অথবা এমন write প্যাটার্ন যা অনেক downstream effect ট্রিগার করে)।
কখন কোনটা ব্যবহার করবেন
Commands ব্যবহার করুন যখন:
- এগিয়ে যাওয়ার আগে জানতে হবে অপারেশনটা সফল হলো কিনা
- অপারেশনটা নির্দিষ্ট একটা service-এর দিকে লক্ষ্য করা
- প্রেরককে failure handle করতে হবে (retry, compensate)
Events ব্যবহার করুন যখন:
- কী ঘটল তা নিয়ে একাধিক service আগ্রহী
- প্রেরকের ফলাফল জানার দরকার নেই
- আপনি চান service-গুলো decouple থাকুক যাতে তারা স্বাধীনভাবে evolve করতে পারে
- কী ঘটল তার একটা audit trail দরকার
Queries ব্যবহার করুন যখন:
- আপনার current state দরকার
- response synchronously দরকার
- অপারেশনটা read-only (কোনো side effect নেই)
এগুলো ইচ্ছাকৃতভাবে মিশিয়ে ফেলা ঠিক আছে — একটা HTTP request (query) যা একটা command ট্রিগার করে যা একটা event emit করে, এটা একটা প্রচলিত ও সঠিক প্যাটার্ন। শুধু naming আর semantics পরিষ্কার হওয়া দরকার।