Part 01 · Low Level Design

What Is Low Level Design?

LLD vs HLDInterview formatRequirements to classes

Code that works today vs code you can still change next month: where LLD fits next to HLD, what a 45-minute LLD round looks like, and how to turn requirements into classes.

⏱ 1-2 hours · Station 1 of 80

Same task, two programs

A college library in Dhaka wants a small borrowing system: members borrow books, a member can hold at most 3 books, a loan lasts 14 days, and a late return costs 5 taka a day.

Sabbir and Tania both build it in an afternoon. Both versions work. Both pass the same five manual checks.

A month later the librarian asks for two changes: premium members can borrow 5 books, and a member who owes a fine can't borrow until they pay.

  • Sabbir's code is one long function with a few global maps. The limit 3 appears in four places, and "is this member allowed?" is checked inside the borrow screen, the renewal screen and the nightly report. The change takes two days and breaks renewals.
  • Tania's code has a Member that knows its own limit and a Library that is the only place deciding who may borrow. The change is two small edits and one new test.

Both programs worked. Only one was designed. The difference between them is what this course calls low level design.

The problem: working code is only day one

Code that works today will be read, changed and extended for years, usually by someone else. Here is what happens to a small system in its first year:

What happensWhat it needs from the code
New rules arrive ("premium gets 5")One obvious place to change each rule
New people join the teamNames and boundaries a stranger can follow
A bug shows up in finesSmall pieces you can test on their own
Two gates/requests run at onceClear ownership of shared state
The product grows a new featureRoom to add code without rewriting old code

None of this shows up on day one. All of it decides how expensive the next change is. Low level design is the habit of shaping code so the next change is cheap, without building things nobody asked for.

Think of a site plan and a floor plan

Before a housing project is built, there are two kinds of drawings.

The site plan shows the whole area: where each building goes, the roads between them, the water and power lines. That's high level design (HLD): which services and databases exist and how they talk.

The floor plan shows the inside of one building: the rooms, which room opens into which, where the kitchen is, and why the bathroom isn't next to the dining table. That's low level design (LLD): the classes inside one service, what each one is responsible for, and how they work together.

AppLibrary APIPostgresEmailLibraryLend · ReturnMemberlimit()BookonLoanLoandue daylendHLD: which boxes exist and how they talkzoom intoone boxLend()LLD: which classes, and who decides what
Press play. First a request crosses the whole system (HLD: app, API, database). Then the view zooms into one box, the library API, and a Lend call crosses the classes inside it (LLD).

Where the analogy stops working. Walls are expensive to move, so architects get the floor plan right first. In software, moving a "wall" (a responsibility from one class to another) is cheap if the code was designed for it, and very expensive if it wasn't. LLD is less about a perfect first drawing and more about keeping the rooms easy to rearrange.

Predict first

In Tania's design, the Member class has a limit() method and the Library is the only place that checks it. The librarian says premium members may now borrow 5 books. How many places in the code change?

The building blocks

  1. Low level design is deciding the classes (or structs, modules) inside one program: what each one holds, what it does, what it is responsible for, and how they relate and collaborate. The output is a class diagram, a few key sequences, and code.

  2. High level design decides the boxes: services, databases, caches, queues, and the arrows between them. It worries about scale, failure and latency across machines.

    HLDLLD
    Zoom levelThe whole systemInside one service or module
    Main piecesServices, DBs, caches, queuesClasses, interfaces, enums
    Typical question"Where does this data live, and how do 1M users reach it?""Which class decides this, and what changes when the rule changes?"
    Goes wrong asOutages, slow pages, lost dataBugs, scary changes, code nobody wants to touch
  3. A good design is easy to change in the ways that are likely. Not every way: the ways the business is actually going to ask for. Most LLD skill is guessing those well and leaving the rest simple.

  4. From requirements to classes: underline the nouns (book, member, loan, fine), they're candidate classes. The verbs (borrow, return, pay) become methods. The rules (limit 3, 14 days, 5 taka/day) need a single home: put each rule in the object that owns the data it depends on.

  5. One owner per decision. "May this member borrow this book?" is decided in exactly one place. Screens, APIs and scripts ask that place; they don't re-implement it.

Behind the scenes: from a sentence to classes

Step through it

One requirement becomes a design

RequirementNouns and verbsClassesCode

Step 1/5 Start from the sentence

"A member can borrow up to 3 books for 14 days. A late return costs 5 taka per day."

rule
limit 3, 14 days, 5 taka/day

The picture to keep in your head

Two zoom levels on the same system:

  • Zoomed out (HLD): boxes and arrows. "The library API talks to Postgres and an email service."
  • Zoomed in (LLD): inside one box, a handful of classes, each with a job you can say in one sentence, and one clear owner for every decision.

When you design, keep asking two questions: who owns this decision? and what will change next? Everything else in this part of the course, from OOP to SOLID to design patterns, is a tool for answering those two questions well.

Try it: LLD or HLD?

Try each case

Which zoom level does this question belong to?

database choice, sharding, replicas
breaks: HLD. It's about storage across machines, not about classes.

Code from first principles

Tania's version of the library, in six languages. Four types from the nouns, two methods from the verbs, and each rule in one place: the limit in Member.limit(), the loan length and fine next to Lend and Return.

The program plays a short story: Rahim borrows until he hits his limit, Nadia tries a book that's taken, Rahim returns late and pays a fine, and premium Nadia borrows.

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())
	}
}

All six print exactly this:

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)

Reading the output:

  • Rahim's fourth book is refused with limit reached (3). The number comes from limit(), so the message is always right, even after the rule changes.
  • Nadia can't take Himu because Rahim has it. Book knows it's on loan; Library checks it before anything else changes.
  • Day 19: the loan was due on day 15 (day 1 + 14), so Rahim is 4 days late: 4 × 5 = 20 taka.
  • Nadia borrows Tintin with a limit of 5. Premium is just a field on Member; no other code needed to know.
  • What each language changed: Go and Python use plain structs with methods; Java uses a record for Loan because it never changes; Rust refers to books and members by index so the Library can own all the data without fighting the borrow checker; C++ keeps pointers into vectors that never resize while loans exist. The design (who owns which rule) is identical in all six.

Real engineering

  • Design for the changes you can see coming. Talk to whoever owns the product: which rules change often? Those deserve a clear home. The rest can stay simple until they move.
  • Write the class list before the code. Five lines ("Library: lends and returns, owns loans. Member: knows its limit…") catch most design mistakes before they cost anything.
  • Keep business rules away from screens and handlers. A borrow API, a librarian screen and a nightly script should all call Library.Lend. (Backend First Principles covers the same idea as "business logic vs plumbing".)
  • Don't build for imagined futures. No IBook interface with one implementation, no factory for two-field objects. You can add them in an afternoon when a real need appears.

The 45-minute LLD interview, in practice:

MinutesWhat you do
0–5Clarify requirements and scope. Ask what's out of scope.
5–12List entities with one-line responsibilities; sketch relationships.
12–20Draw the class diagram; name the key methods.
20–38Code the core flow (here: Lend and Return), with the rules in their homes.
38–45Handle "what if…" follow-ups: new rule, new type, two threads at once.

The last block is where interviews are won: if your design lets you answer "what if premium members get 5?" with "one line in limit()", you've shown exactly what LLD is for. The last chapter of this part goes through this method in detail.

When it breaks

Try each case

Pick a design smell

func handleBorrow(req) { ... 300 lines ... }
breaks: Reading input, checking rules, updating maps, printing receipts, all in one place. Every change touches it, and nothing can be tested alone. Split by responsibility.

Think before you read on

A teammate says: "Let's make Book an abstract class now, with PaperBook and EBook subclasses. The library might lend e-books one day." The library has never mentioned e-books. What do you say?

Reveal

Not yet. Today there is one kind of book and no behaviour that differs. Subclasses now would be guesses, and guesses are usually wrong in the details (do e-books have copies? loan limits?). Keep Book simple. When e-books become a real requirement, you'll know how they actually differ, and adding the abstraction then is a small change, because the rest of the design (one owner per rule) is clean. That's YAGNI: "you aren't gonna need it", until you do.

Check yourself

1. In one sentence each, what does HLD decide and what does LLD decide?

Answer

HLD decides which services, databases and queues exist and how they talk across machines. LLD decides which classes exist inside one program, what each is responsible for, and how they collaborate.

2. Both Sabbir's and Tania's programs worked. What made Tania's better?

Answer

Each rule had one home (the limit on Member, lending rules in Library), so a new rule meant a small, safe change instead of hunting through copies.

3. How do you get from a requirement sentence to candidate classes and methods?

Answer

Nouns become candidate classes, verbs become methods, and each rule goes to the object that owns the data it depends on. Then remove any class that has no state or behaviour of its own.

4. In a 45-minute LLD interview, where should most of the time go, and what wins the last few minutes?

Answer

Most of it goes to the class design and coding the core flow. The last minutes are won by handling "what if" follow-ups with small, local changes, which shows the design is easy to change.

Your challenge: the fine rule

Add the librarian's second request: a member who owes a fine can't borrow until they pay.

  • Give Member the amount it owes. Return adds the fine to it.
  • Lend refuses with pay fine first (20 taka) when the member owes anything.
  • Add one line at the end of the story: Rahim tries to borrow Himu again on day 19.

Before you run it, predict the new last line.

You've succeeded when the last line is day 19 Rahim Himu refused: pay fine first (20 taka), every earlier line prints exactly as before, and the check lives in one place (Lend), not in the story code.

The mental model

HLD draws the boxes; LLD designs what's inside one box. Turn nouns into classes and verbs into methods, give every rule exactly one home, and keep the design just flexible enough for the changes you can actually see coming. Working code is day one; good design is every day after.

Station stamp

Finish the chapter

Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.

Go Pro to earn XP →