Part 01 · Low Level Design

What Is Low Level Design?

LLD vs HLDInterview formatRequirements to classes

আজকে চলে এমন code আর এক মাস পরেও বদলানো যায় এমন code: HLD এর পাশে LLD কোথায় বসে, ৪৫ মিনিটের একটা LLD round কেমন হয়, আর requirement থেকে class এ যাওয়ার রাস্তা।

⏱ 1-2 ঘণ্টা · Station 1 / 80

একই কাজ, দুইটা program

ঢাকার একটা কলেজ library ছোট একটা system চায়। Member রা বই নেবে। একজন একসাথে ৩টার বেশি বই রাখতে পারবে না। বই রাখা যাবে ১৪ দিন। দেরি হলে দিনে ৫ টাকা fine।

সাব্বির আর তানিয়া দুজনেই এক বিকেলে এটা বানিয়ে ফেললো। দুটোই চলে। হাতে-কলমে একই পাঁচটা check দিলে দুটোই pass।

এক মাস পর librarian এসে দুইটা জিনিস চাইলেন: premium member রা ৫টা বই নিতে পারবে, আর কারো fine বাকি থাকলে টাকা না দেওয়া পর্যন্ত সে বই নিতে পারবে না।

  • সাব্বিরের code একটা long function, সাথে কয়েকটা global map। 3 লেখা আছে চার জায়গায়। "এই member বই নিতে পারবে কিনা" সেটা check হচ্ছে borrow screen এ, renew screen এ, আবার রাতের report এও। এই change করতে তার দুই দিন লাগলো, আর renew এর feature টা ভেঙে গেল।
  • তানিয়ার code এ Member নিজের limit নিজে জানে। আর কে বই পাবে, সেটা ঠিক করে শুধু Library। তার change হলো দুইটা ছোট edit আর একটা নতুন test।

দুইটা program ই চলতো। কিন্তু design ছিল একটাতেই। এই পার্থক্যটার নামই low level design, আর এই course এ আমরা এটাই শিখবো।

সমস্যাটা কোথায়: code চললেই কাজ শেষ না

আজ যে code লিখলে, সেটা সামনের কয়েক বছর মানুষ পড়বে, বদলাবে, তার উপর নতুন জিনিস বানাবে। আর বেশিরভাগ সময় সেই মানুষটা তুমি না, অন্য কেউ। একটা ছোট system এর প্রথম বছরেই সাধারণত এগুলো হয়:

যা হয়তখন code এর কাছে যা লাগে
নতুন rule আসে ("premium পাবে ৫টা")প্রতিটা rule বদলানোর একটা পরিষ্কার জায়গা
Team এ নতুন কেউ join করেএমন নাম আর ভাগ, যেটা নতুন মানুষও ধরতে পারে
Fine এর হিসাবে bug ধরা পড়েছোট ছোট অংশ, যেগুলো আলাদা করে test করা যায়
একসাথে দুইটা request আসেShared data কার হাতে, সেটা পরিষ্কার
নতুন feature লাগেপুরনো code না ভেঙে নতুন code বসানোর জায়গা

প্রথম দিন এর কিছুই চোখে পড়ে না। কিন্তু পরের change টা কতটা কষ্টের হবে, সেটা এগুলোই ঠিক করে দেয়। Low level design মানে code টা এমনভাবে সাজানো যাতে পরের change টা সহজ হয়, আবার কেউ চায়নি এমন জিনিসও বানাতে না হয়।

একটা building বানানোর কথা ভাবো

একটা housing project শুরুর আগে দুই রকম plan লাগে।

একটা হলো site plan। পুরো জায়গাটা উপর থেকে: কোন building কোথায় বসবে, মাঝখান দিয়ে রাস্তা কোথায়, পানি আর কারেন্টের লাইন কোন দিক দিয়ে যাবে। Software এ এটাই high level design (HLD): কোন কোন service আর database থাকবে, আর তারা কীভাবে একে অপরের সাথে কথা বলবে।

আরেকটা হলো floor plan। একটা building এর ভেতরটা: কয়টা রুম, কোন রুম থেকে কোন রুমে যাওয়া যায়, রান্নাঘর কোথায়, বাথরুম কেন ডাইনিং টেবিলের পাশে না। এটাই low level design (LLD): একটা service এর ভেতরে কোন কোন class, কার কী কাজ, আর তারা মিলে কীভাবে কাজটা করে।

AppLibrary APIPostgresEmailLibraryLend · ReturnMemberlimit()BookonLoanLoandue daylendHLD: which boxes exist and how they talkzoom intoone boxLend()LLD: which classes, and who decides what
Play চাপো। প্রথমে একটা request পুরো system পার হয় (HLD: app, API, database)। তারপর আমরা একটা box এর ভেতরে ঢুকি, library API তে, আর একটা Lend call ভেতরের class গুলোর মধ্যে দিয়ে যায় (LLD)।

তবে একটা জায়গায় মেলে না। বাস্তবের দেয়াল ভাঙা অনেক খরচের, তাই architect রা floor plan আগেই পাকা করে নেয়। Software এ একটা "দেয়াল" সরানো, মানে এক class এর কাজ আরেক class এ নিয়ে যাওয়া, সস্তা, যদি code টা সেভাবে লেখা থাকে। না থাকলে সেটাই সবচেয়ে কষ্টের। তাই LLD মানে প্রথমবারেই সব ঠিক করে ফেলা না। LLD মানে রুমগুলো এমনভাবে রাখা যাতে দরকার হলে সহজে আবার সাজানো যায়।

আগে অনুমান করো

তানিয়ার code এ Member এর একটা limit() method আছে, আর Library ছাড়া আর কেউ এটা check করে না। এখন librarian বললেন premium member রা ৫টা বই নিতে পারবে। Code এর কয় জায়গায় হাত দিতে হবে?

LLD আসলে কী, আর HLD থেকে কোথায় আলাদা

  1. Low level design হলো একটা program এর ভেতরের class (বা struct, module) গুলো ঠিক করা: কে কী data রাখে, কী কাজ করে, কোন সিদ্ধান্তটা তার, আর অন্যদের সাথে কীভাবে জোড়া আর কীভাবে মিলে কাজ করে। শেষে হাতে থাকে একটা class diagram, জরুরি কয়েকটা flow, আর code।

  2. High level design ঠিক করে box গুলো: service, database, cache, queue, আর এদের মাঝের তীর। এখানে মাথাব্যথা হলো অনেক machine জুড়ে scale, failure আর latency।

    HLDLLD
    কোথা থেকে দেখছোপুরো systemএকটা service বা module এর ভেতরে
    প্রধান অংশService, DB, cache, queueClass, interface, enum
    সাধারণ প্রশ্ন"এই data কোথায় থাকবে, আর ১০ লাখ user এটা কীভাবে পাবে?""এই সিদ্ধান্ত কোন class নেবে, আর rule বদলালে কী কী বদলাতে হবে?"
    ভুল হলে যা হয়Site down, page ধীর, data হারায়Bug, change করতে ভয় লাগে, কেউ code টা ছুঁতে চায় না
  3. ভালো design মানে যে change গুলো আসার কথা, সেগুলো সহজে করা যায়। দুনিয়ার সব change না, business আসলে যেগুলো চাইবে সেগুলো। LLD এর আসল skill হলো এগুলো ঠিকঠাক ধরতে পারা, আর বাকি সব সহজ রাখা।

  4. Requirement থেকে class এ যাওয়া: requirement এর noun গুলো দাগাও (book, member, loan, fine), এগুলো class হতে পারে। Verb গুলো (borrow, return, pay) হয় method। আর rule গুলো (৩টা বই, ১৪ দিন, দিনে ৫ টাকা) কোথায় বসবে? যে object এর data দেখে rule টা চলে, তার কাছে।

  5. প্রতিটা সিদ্ধান্তের মালিক একজনই। "এই member এই বইটা পাবে কিনা" এটা ঠিক হবে এক জায়গায়। Screen, API, script সবাই ওই জায়গাকেই জিজ্ঞেস করবে, কেউ নিজে আবার rule টা লিখবে না।

Requirement এর একটা লাইন থেকে class বানানো

ধাপে ধাপে দেখো

একটা requirement থেকে design

RequirementNoun আর verbClassCode

ধাপ 1/5 Requirement এর লাইনটা দিয়ে শুরু

"একজন member ১৪ দিনের জন্য সর্বোচ্চ ৩টা বই নিতে পারবে। দেরিতে ফেরত দিলে দিনে ৫ টাকা fine।"

rule
limit 3, 14 days, 5 taka/day

এক নজরে: একই system, দুই level

একই system, দুই জায়গা থেকে দেখা:

  • দূর থেকে (HLD): কয়েকটা box আর তীর। "Library API কথা বলে Postgres আর একটা email service এর সাথে।"
  • কাছে গিয়ে (LLD): একটা box এর ভেতরে অল্প কয়েকটা class। প্রত্যেকের কাজ এক লাইনে বলা যায়, আর প্রতিটা সিদ্ধান্তের মালিক একজন।

Design করার সময় নিজেকে দুইটা প্রশ্ন বারবার করো: এই সিদ্ধান্তটা কার? আর এরপর কী বদলাতে পারে? এই part এ সামনে যা আসছে, OOP, SOLID, design pattern, সবই এই দুইটা প্রশ্নের ভালো উত্তর দেওয়ার tool।

নিজে বলো: এটা LLD না HLD?

প্রতিটা case চালিয়ে দেখো

এই প্রশ্নটা কোন level এর?

database বাছাই, sharding, replica
ভাঙে: HLD। অনেক machine জুড়ে data রাখার প্রশ্ন, class এর না।

Code এ তানিয়ার library

তানিয়ার library টা, ছয়টা language এ। Noun থেকে চারটা type, verb থেকে দুইটা method। আর প্রতিটা rule এক জায়গায়: limit আছে Member.limit() এ, loan এর মেয়াদ আর fine আছে Lend আর Return এর পাশে।

Program টা ছোট একটা গল্প চালায়: রহিম limit শেষ না হওয়া পর্যন্ত বই নেয়, নাদিয়া এমন একটা বই চায় যেটা আগেই কেউ নিয়ে গেছে, রহিম দেরিতে ফেরত দিয়ে fine দেয়, তারপর premium member নাদিয়া বই নেয়।

package main

import (
	"errors"
	"fmt"
)

// Each class has one job, found from the nouns in the requirements.

type Book struct {
	Title  string
	onLoan bool
}

type Member struct {
	Name    string
	Premium bool
	loans   int
}

// The borrowing limit lives with the member, so "premium gets 5" is a one-line change.
func (m *Member) limit() int {
	if m.Premium {
		return 5
	}
	return 3
}

type Loan struct {
	book   *Book
	member *Member
	due    int // day number
}

// Library is the only place that decides who may borrow what.
type Library struct {
	loans []*Loan
}

const loanDays, finePerDay = 14, 5 // taka

func (l *Library) Lend(m *Member, b *Book, today int) error {
	if b.onLoan {
		return errors.New("already on loan")
	}
	if m.loans >= m.limit() {
		return fmt.Errorf("limit reached (%d)", m.limit())
	}
	b.onLoan = true
	m.loans++
	l.loans = append(l.loans, &Loan{book: b, member: m, due: today + loanDays})
	return nil
}

// Return closes the loan and says how much fine is due.
func (l *Library) Return(b *Book, today int) (int, error) {
	for i, ln := range l.loans {
		if ln.book == b {
			l.loans = append(l.loans[:i], l.loans[i+1:]...)
			b.onLoan = false
			ln.member.loans--
			return max(0, today-ln.due) * finePerDay, nil
		}
	}
	return 0, errors.New("not on loan")
}

func main() {
	lib := &Library{}
	rahim := &Member{Name: "Rahim"}
	nadia := &Member{Name: "Nadia", Premium: true}
	books := []*Book{{Title: "Himu"}, {Title: "Feluda"}, {Title: "Byomkesh"}, {Title: "Tintin"}}

	for _, b := range books {
		if err := lib.Lend(rahim, b, 1); err != nil {
			fmt.Printf("day 1  Rahim  %-9s refused: %v\n", b.Title, err)
		} else {
			fmt.Printf("day 1  Rahim  %-9s ok\n", b.Title)
		}
	}
	if err := lib.Lend(nadia, books[0], 2); err != nil {
		fmt.Printf("day 2  Nadia  %-9s refused: %v\n", books[0].Title, err)
	}
	fine, _ := lib.Return(books[0], 19) // due on day 15
	fmt.Printf("day 19 Rahim  returns %s, %d days late, fine %d taka\n", books[0].Title, fine/finePerDay, fine)
	if err := lib.Lend(nadia, books[3], 19); err == nil {
		fmt.Printf("day 19 Nadia  %-9s ok (premium limit %d)\n", books[3].Title, nadia.limit())
	}
}

ছয়টাই হুবহু এটা print করে:

day 1  Rahim  Himu      ok
day 1  Rahim  Feluda    ok
day 1  Rahim  Byomkesh  ok
day 1  Rahim  Tintin    refused: limit reached (3)
day 2  Nadia  Himu      refused: already on loan
day 19 Rahim  returns Himu, 4 days late, fine 20 taka
day 19 Nadia  Tintin    ok (premium limit 5)

Output টা পড়ি:

  • রহিমের চার নম্বর বইটা দেওয়া হয়নি, message limit reached (3)। সংখ্যাটা আসছে limit() থেকে, তাই rule বদলালেও message ভুল হবে না।
  • নাদিয়া হিমু পায়নি, কারণ ওটা রহিমের কাছে। বইটা loan এ আছে কিনা Book নিজেই জানে, আর কিছু বদলানোর আগেই Library সেটা দেখে নেয়।
  • ১৯ তারিখ: বই ফেরতের শেষ দিন ছিল ১৫ (১ + ১৪)। রহিম ৪ দিন দেরি করেছে, তাই ৪ × ৫ = ২০ টাকা।
  • নাদিয়া টিনটিন পেয়েছে, কারণ তার limit ৫। Premium শুধু Member এর একটা field, আর কোনো code কে এটা নিয়ে মাথা ঘামাতে হয়নি।
  • Language ভেদে কী আলাদা: Go আর Python এ সাধারণ struct আর method। Java তে Loan একটা record, কারণ এটা কখনো বদলায় না। Rust এ book আর member কে index দিয়ে ধরা হয়েছে, যাতে borrow checker এর সাথে যুদ্ধ না করে Library সব data নিজের কাছে রাখতে পারে। C++ এ vector এর ভেতরে pointer রাখা, আর loan চলাকালীন vector গুলো resize হয় না। কিন্তু কোন rule কার কাছে, সেই design ছয়টাতেই এক।

Real project আর interview এ যা কাজে লাগে

  • যে change গুলো আসছে বলে বোঝা যাচ্ছে, সেগুলোর জন্য design করো। Product টা যিনি দেখেন, তাকে জিজ্ঞেস করো: কোন rule গুলো ঘনঘন বদলায়? ওগুলোর একটা পরিষ্কার জায়গা লাগবে। বাকিগুলো যতদিন না নড়ে, সাধারণই থাকুক।
  • Code লেখার আগে class গুলোর list লিখে ফেলো। পাঁচ লাইন ("Library: বই দেয়, ফেরত নেয়, loan গুলো তার কাছে। Member: নিজের limit জানে…") লিখলেই বেশিরভাগ design এর ভুল আগেভাগে ধরা পড়ে।
  • Business rule কে screen আর handler থেকে দূরে রাখো। Borrow API, librarian এর screen, রাতের script, সবাই Library.Lend কে ডাকবে। (Backend First Principles এ এটাই আছে "business logic বনাম plumbing" নামে।)
  • কাল্পনিক ভবিষ্যতের জন্য কিছু বানিও না। একটাই implementation এর জন্য IBook interface না, দুই field এর object এর জন্য factory না। সত্যি দরকার পড়লে এগুলো এক বিকেলেই বসানো যায়।

৪৫ মিনিটের LLD interview, বাস্তবে যেমন যায়:

মিনিটকী করবে
০–৫Requirement আর scope পরিষ্কার করো। কী কী এবার বানাতে হবে না, সেটাও জিজ্ঞেস করো।
৫–১২Entity গুলোর list, প্রত্যেকের কাজ এক লাইনে; কে কার সাথে জোড়া, এঁকে ফেলো।
১২–২০Class diagram আঁকো, মূল method গুলোর নাম দাও।
২০–৩৮মূল flow টা code করো (এখানে Lend আর Return), rule গুলো তাদের জায়গায় রেখে।
৩৮–৪৫"যদি এমন হয়…" ধরনের প্রশ্ন সামলাও: নতুন rule, নতুন type, একসাথে দুইটা thread।

Interview টা আসলে জেতা হয় শেষের অংশে। "Premium member রা ৫টা পেলে?" প্রশ্নে যদি বলতে পারো "limit() এ এক লাইন", তাহলে তুমি ঠিক সেটাই দেখালে যেটা দেখানোর জন্য LLD round। এই part এর শেষ chapter এ এই পুরো approach টা ধরে ধরে আছে।

যে design গুলো পরে problem create করে

প্রতিটা case চালিয়ে দেখো

খারাপ design টা চেনো

func handleBorrow(req) { ... 300 lines ... }
ভাঙে: Input পড়া, rule check, map update, রসিদ print, সব এক জায়গায়। যেকোনো change এ এটাকেই ধরতে হয়, আর কোনো অংশ আলাদা করে test করা যায় না। কাজ ধরে ভাগ করো।

একটু থেমে ভাবো

একজন teammate বললো: "চলো Book কে এখনই abstract class বানিয়ে ফেলি, সাথে PaperBook আর EBook subclass। Library হয়তো কোনোদিন e-book দেবে।" অথচ library কখনো e-book এর কথা বলেনি। তুমি কী বলবে?

উত্তর দেখো

এখন না। আজ বই একরকমই, আলাদা কোনো আচরণও নেই। এখন subclass বানানো মানে আন্দাজে বানানো, আর আন্দাজ খুঁটিনাটিতে প্রায়ই ভুল হয় (e-book এর কি copy থাকে? limit আছে?)। Book যেমন আছে তেমন রাখো। E-book যেদিন সত্যি requirement হয়ে আসবে, সেদিন জানবে আসলে পার্থক্য কোথায়। তখন abstraction বসানো ছোট একটা কাজ, কারণ বাকি design (প্রতিটা rule এর মালিক একজন) পরিষ্কার। এটাকেই বলে YAGNI: "you aren't gonna need it", যতদিন না সত্যি লাগে।

ছোট্ট quiz

১. এক লাইনে বলো, HLD কী ঠিক করে আর LLD কী ঠিক করে?

উত্তর

HLD ঠিক করে কোন কোন service, database আর queue থাকবে, আর অনেক machine জুড়ে তারা কীভাবে কথা বলবে। LLD ঠিক করে একটা program এর ভেতরে কোন কোন class থাকবে, কার কী দায়িত্ব, আর তারা মিলে কীভাবে কাজ করবে।

২. সাব্বির আর তানিয়া দুজনের program ই চলতো। তানিয়ারটা ভালো কেন?

উত্তর

প্রতিটা rule এর জায়গা ছিল একটাই (limit আছে Member এ, বই দেওয়ার নিয়ম Library তে)। তাই নতুন rule মানে ছোট, নিরাপদ একটা change, copy খুঁজে খুঁজে বের করা না।

৩. একটা requirement এর লাইন থেকে class আর method এ যাও কীভাবে?

উত্তর

Noun গুলো class হতে পারে, verb গুলো method হয়, আর প্রতিটা rule যায় সেই object এর কাছে যার data দেখে rule টা চলে। তারপর যে class এর নিজের কোনো data বা কাজ নেই, সেটা বাদ দাও।

৪. ৪৫ মিনিটের LLD interview তে বেশিরভাগ সময় কোথায় দেবে, আর শেষের কয়েক মিনিট কী দিয়ে জেতা যায়?

উত্তর

বেশিরভাগ সময় যায় class design আর মূল flow code করতে। শেষ কয়েক মিনিট জেতা যায় "যদি এমন হয়…" প্রশ্নগুলো ছোট, এক জায়গার change দিয়ে সামলে। তাতে বোঝা যায় design টা সহজে বদলানো যায়।

তোমার challenge: fine এর rule

Librarian এর দ্বিতীয় দাবিটা যোগ করো: যার fine বাকি, টাকা না দেওয়া পর্যন্ত সে বই নিতে পারবে না।

  • Member কে তার বাকি টাকার হিসাব রাখতে দাও। Return fine টা ওখানে যোগ করবে।
  • Member এর কিছু বাকি থাকলে Lend বই দেবে না, বলবে pay fine first (20 taka)।
  • গল্পের শেষে এক লাইন যোগ করো: ১৯ তারিখে রহিম আবার হিমু নিতে চায়।

চালানোর আগে আন্দাজ করো, নতুন শেষ লাইনটা কী আসবে।

হয়েছে বুঝবে যখন শেষ লাইনে আসবে day 19 Rahim Himu refused: pay fine first (20 taka), আগের সব লাইন হুবহু আগের মতো থাকবে, আর check টা থাকবে এক জায়গায় (Lend এ), গল্পের code এ না।

এক কথায়

HLD বাইরের box গুলো আঁকে; LLD একটা box এর ভেতরটা সাজায়। Noun থেকে class, verb থেকে method, প্রতিটা rule এর জন্য ঠিক একটা জায়গা, আর design কে ততটুকুই flexible রাখো যতটুকু change সত্যি আসতে দেখছো। Code চালানো প্রথম দিনের কাজ; ভালো design লাগে তার পরের প্রতিটা দিনে।

Station stamp

Chapter শেষ করো

পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।

Pro নিয়ে XP পাও →