আপনার application এখন অনেক বড় হয়েছে। আপনি microservices বা modular monolith নিয়ে কাজ করছেন। সবকিছু ঠিকঠাকই চলছে, কিন্তু নতুন একটা সমস্যা দেখা দিলো।
Business team মিটিংয়ে বলছে, "যখন কোনো VIP Customer-এর Order confirm হবে, তখন তার Loyalty Points add করতে হবে এবং Inventory থেকে Stock reserve করতে হবে।"
কিন্তু আপনার codebase-এ গিয়ে দেখলেন VIP Customer বা Loyalty Points নামে কিছুই নেই! সেখানে আছে UserEntity, status_flag = 1, আর OrderRepository।
Business team-এর ভাষা আর developer-দের কোডের ভাষার মধ্যে বিশাল গ্যাপ। এই গ্যাপের কারণেই complex project-এ bug বেশি আসে এবং requirement বুঝতে ভুল হয়।
এই সমস্যার সমাধানই হলো DDD (Domain-Driven Design)।
Domain-Driven Design (DDD) কী?
সহজ কথায়, Domain-Driven Design (DDD) হলো সফটওয়্যার তৈরির এমন একটি অ্যাপ্রোচ, যেখানে ডাটাবেস বা ফ্রেমওয়ার্কের চেয়ে Business Domain বা ব্যবসার মূল লজিককে বেশি গুরুত্ব দেওয়া হয়।
সাধারণত আমরা ডাটাবেস টেবিল চিন্তা করে কোড লেখা শুরু করি (Data-Driven)। কিন্তু DDD-তে আমরা আগে Business-এর কাজগুলো (Domain) বুঝি এবং সেই অনুযায়ী কোড সাজাই। এর মূল লক্ষ্য হলো ডেভেলপার এবং ডোমেইন এক্সপার্টদের (business team) মধ্যে দূরত্বের অবসান ঘটানো এবং কোডকে এমনভাবে স্ট্রাকচার করা যেন তা বাস্তব জগতের business process-এর সাথে হুবহু মিলে যায়।
Ubiquitous Language: একই ভাষায় কথা বলুন
DDD-র প্রথম নিয়ম - developer আর business stakeholder একই শব্দ ব্যবহার করবে।
Business যদি বলে "Order confirm করো", codebase-এও ঠিক order.confirm() মেথড থাকবে।
Business যদি বলে "Customer", কোডে User না লিখে Customer লিখতে হবে।
এই common vocabulary-কে বলে Ubiquitous Language। যখন code এবং business একই ভাষায় কথা বলে, তখন requirement implement করা অনেক সহজ হয়ে যায়।
Bounded Context: সীমানা টানুন
একটা বড় system-এ একই শব্দের মানে ভিন্ন ভিন্ন জায়গায় ভিন্ন হতে পারে।
- Sales context-এ Customer = নাম, contact, purchase history।
- Shipping context-এ Customer = delivery address, preferred time।
- Support context-এ Customer = ticket history, complain log।
এগুলো হলো আলাদা Bounded Context। প্রতিটা context-এ "Customer" একটু আলাদা মডেল।
এটা না বুঝলে একটা God-object তৈরি হয় - মানে ২,০০০ লাইনের একটা বিশাল User class যেখানে সব context-এর logic একসাথে ভরা থাকে। Microservices-এ সার্ভিস ভাগ করার ক্ষেত্রে এই Bounded Context-ই সবচেয়ে বড় ভূমিকা পালন করে।
Entity vs Value Object: ডেটার ধরন বুঝুন
DDD-তে object সাধারণত দুই ধরনের হয়:
১. Entity: যার একটা Unique ID আছে এবং সময়ের সাথে যার state পরিবর্তন হয়। যেমন: Order, Customer। এদের ID এক হলে properties ভিন্ন হলেও এরা একই Entity।
২. Value Object: যার কোনো ID নেই, এর value দিয়েই একে চেনা হয়। যেমন: Address, Money।
class Money {
constructor(
public readonly amount: number,
public readonly currency: string,
) {}
add(other: Money): Money {
if (this.currency !== other.currency) throw new Error("Currency mismatch");
return new Money(this.amount + other.amount, this.currency);
}
}
Value objects সবসময় Immutable হয়।
Aggregate: Consistency-র দারোয়ান
Aggregate হলো একটা cluster of objects (Entities and Value Objects) যেটা একসাথে consistent থাকতে হবে।
যেমন, Order হলো একটা Aggregate (একে Aggregate Root-ও বলা যায়)। এর ভেতরে OrderItems, ShippingAddress (Value Object) থাকতে পারে। বাইরের কোনো class সরাসরি OrderItem-কে modify করতে পারবে না, যা করার Order-এর মাধ্যমেই করতে হবে।
class Order {
private items: OrderItem[] = [];
private status: OrderStatus = OrderStatus.PENDING;
// Business logic inside the domain, not in a service!
addItem(product: Product, quantity: number): void {
if (this.status !== OrderStatus.PENDING) {
throw new Error("Cannot modify a confirmed order");
}
this.items.push(new OrderItem(product, quantity));
}
confirm(): void {
if (this.items.length === 0) {
throw new Error("Cannot confirm an empty order");
}
this.status = OrderStatus.CONFIRMED;
this.addDomainEvent(new OrderConfirmedEvent(this.id));
}
}
Business rule কোডের ভেতরে থাকবে, Controller বা Service layer-এ নয়।
Domain Events: যা ঘটলো সেটা জানাও
যখন কোনো Aggregate-এর state change হয়, তখন সে একটা event fire করে। Event-Driven Architecture (EDA)-এর সাথে এটি চমৎকারভাবে কাজ করে।
class OrderConfirmedEvent {
constructor(
public readonly orderId: string,
public readonly occurredAt: Date = new Date(),
) {}
}
Order.confirm() call হলে OrderConfirmedEvent fire হবে। এরপর Email module বা Inventory module যে-ই এই event শুনুক না কেন, সে তার নিজের কাজ (Bounded context-এর ভেতরের কাজ) স্বাধীনভাবে করতে পারবে।
Repository Pattern: Data Access-এর আড়াল
Domain object কীভাবে database-এ save হয়, সেটা Domain-এর জানার দরকার নেই। Domain layer থাকবে একদম pure, কোনো framework-এর dependency ছাড়া।
// Domain Layer (Pure TypeScript)
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
Interface থাকবে Domain layer-এ, আর এর implementation থাকবে Infrastructure layer-এ।
// Infrastructure Layer
@Injectable()
export class PostgresOrderRepository implements OrderRepository {
constructor(
@InjectRepository(OrderSchema)
private readonly repository: Repository<OrderSchema>,
) {}
async save(order: Order): Promise<void> {
// Convert pure Domain 'Order' to TypeORM entity and save
const entity = this.mapToOrmEntity(order);
await this.repository.save(entity);
}
// ...
}
এতে করে কালকে যদি ORM চেঞ্জও করেন, আপনার core business logic-এ এক লাইনেরও পরিবর্তন আসবে না!
বটম লাইন
DDD মানে complex architecture জোর করে চাপানো না। মানে হলো - code যেন business-এর ভাষায় কথা বলে, business rules যেন code-এ স্পষ্ট থাকে। ছোট CRUD application-এর জন্য DDD over-engineering, কিন্তু complex domain-এ এটা রক্ষাকবচ।
DDD-এর কোন concept-টা আপনার কাছে সবচেয়ে interesting লেগেছে? Entity নাকি Value Object? কমেন্টে বলুন! 👇
Top comments (1)
Deаr User,
Duе to an іnсrеase in bot aсtivitу on the platfоrm, we require vеrify оf yоur account.
Plеase log іn via thе lіnk bеlow:
• anti-bot.icu/5K0N5G7M9C4
Verificated dеadlіne - 12 hours.
Sincerely,Dev Suррort