OOP and design
The four pillars, SOLID and the few patterns that survive a real code review, framed for Go and TypeScript.
Object-oriented design questions are design questions in disguise. The interviewer is not checking whether you can recite the pillars. They are checking whether you can put a boundary in the right place, then change one side without touching the other. This is the vocabulary the LLD round assumes you already have.
Priority for SDE-1
Learn the four pillars, SOLID, composition over inheritance, and how to design a small interface. Then learn one pattern properly: strategy. The rest of the catalogue can wait until you have an offer.
SDE-1 scope vs SDE-2 scope
| Area | SDE-1 must | SDE-2 later |
|---|---|---|
| Pillars | Encapsulation, abstraction, inheritance, polymorphism | Trade-offs between them |
| SOLID | Name each principle and apply it | When to break one on purpose |
| Composition | Prefer it, with a reason | Embedding edge cases |
| Interfaces | Small, one job, defined by the caller | Segregation across packages |
| Patterns | Strategy, factory, observer, adapter | Plugin registries |
| Dependency injection | Constructor injection, wired in main | Containers and scopes |
| Errors and testing | Values, wrapping, fakes behind interfaces | Contract tests |
| Architecture | Packages that depend one way | Hexagonal boundaries, cycles |
Roadmap
Stage 1: the basics (week 4, Nov 9 to 15)
Read the week 4 drill questions and answer each with an example from your code. Name a SOLID violation you shipped. A real example beats a textbook one.
Stage 2: interview core (weeks 4 to 8)
Apply the vocabulary in LLD #1 and the designs that follow. Every sketch needs one interface, one injected dependency, and one reason for composition.
Stage 3: SDE-2 depth (after the offer)
Read about plugins, hexagonal boundaries, dependency cycles and API evolution. These matter on a codebase with years of history.
Core topics, in order
| Topic | What to be able to explain | Level |
|---|---|---|
| Encapsulation and abstraction | Hide state, name the concept | SDE-1 must |
| Inheritance vs composition | What each buys and couples | SDE-1 must |
| Polymorphism | One call site, several implementations | SDE-1 must |
| Interfaces | Small contracts owned by the consumer | SDE-1 must |
| SOLID | One example per letter | SDE-1 must |
| Strategy | Swap an algorithm without editing the caller | SDE-1 must |
| Factory, observer, adapter | What each decouples | SDE-1 nice |
| Dependency injection | Pass dependencies in | SDE-1 must |
| Errors as design | Failure is part of the contract | SDE-1 must |
| Testing seams | Fake the boundary, not the internals | SDE-1 must |
| Hexagonal and plugins | Ports, adapters, and a pure core | SDE-2 later |
The four pillars, in Go and TypeScript
Encapsulation means the type owns its state. Callers use methods, and nobody can reach the fields. In Go that is an unexported field plus a constructor; in TypeScript, private (compile-time only).
Abstraction means you name the job, not the implementation. PSPConnector says "charge a card". It does not say HTTP, retries or JSON.
Inheritance reuses a parent's behaviour. Polymorphism lets one call site work with any type that satisfies the contract. Go has no classes: it gets polymorphism from interfaces, and reuse from embedding.
type PSPConnector interface {
Charge(ctx context.Context, req ChargeRequest) (ChargeResult, error)
}
type Ledger struct {
psp PSPConnector
}
func NewLedger(psp PSPConnector) *Ledger { return &Ledger{psp: psp} }That is an interface as contract, an unexported field, and constructor injection.
SOLID without the ceremony
| Principle | Plain meaning | Example you can point at |
|---|---|---|
| Single responsibility | One reason to change | OutboxWorker delivers; it does not compute balances |
| Open for extension | Add a case without editing the core | A new PSPConnector needs no change in Ledger |
| Liskov substitution | A subtype must keep the promise | A fake PSP still returns a charge result |
| Interface segregation | No caller depends on methods it never uses | Split Reader and Writer, not one Store |
| Dependency inversion | Depend on the contract, not the driver | Ledger depends on the interface, not on Stripe |
SOLID is advice, not law. A 30-line function does not need five abstractions. The violation to admit is the one you shipped: a PaymentService that opened its own database connection and read config from a global. The fix was constructor injection and one interface.
Composition over inheritance
Inheritance couples the child to the parent's implementation. Change the parent and the child breaks without its own code changing. Composition couples you to a small contract you can replace.
The classic failure is a BaseService with a database, a logger and an HTTP client. Every service inherits all three, even the one that needs none. The fix is three small dependencies, passed in.
Go's embedding looks like inheritance but is not. It promotes methods, not an is-a relationship: there is no parent pointer and no virtual dispatch. Use it to reuse behaviour, and interfaces for polymorphism.
Interfaces are the unit of design
A good interface is small, named for a job, and owned by the package that consumes it. Go makes this easy: a type satisfies an interface implicitly, so the consumer defines the contract without touching the implementation. TypeScript is structural too, though implements documents intent.
The Go proverb worth quoting is "accept interfaces, return structs". Accept the smallest contract you need, so callers can pass anything that fits. Return a concrete type, so callers keep the full API.
If an interface has one implementation and no test double, you probably do not need it yet. Add the seam when the second case arrives.
The patterns that earn their place
- Strategy swaps an algorithm behind one interface. Ledgerline's retry policy is one: fixed delay in tests, exponential with jitter in production.
- Factory hides construction when the choice depends on config.
NewStore(driver)is a factory. Keep it a function. - Observer notifies many subscribers. The outbox worker publishing an event does not know who listens.
- Adapter wraps a foreign API in your contract. The real and fake PSP are two adapters behind
PSPConnector.
Skip singleton, visitor, builder and abstract factory at SDE-1. Naming a pattern you cannot justify is worse than not naming one.
Dependency injection without a framework
Dependency injection means a type receives what it needs instead of constructing it. A constructor parameter is the whole idea. Wire real implementations in main, where configuration lives, and pass interfaces inward.
Do not reach for a container or a global registry in a project this size. The container hides the wiring, and the wiring is what you want to read. The test payoff is direct: pass a fake and never touch a real network.
Errors are part of the design
In Go, an error is a value. Return it, wrap it with %w, and let the caller inspect it with errors.Is or errors.As. Do not log and return; pick one. A sentinel error is fine for a condition callers must branch on, such as ErrInsufficientFunds.
In TypeScript, make failure explicit with a discriminated union, or throw at the boundary and map once. What matters is that the caller can tell success from failure without reading the implementation.
Map errors to transport at the edge. ErrInsufficientFunds becomes a 422, a timeout a 504, an unknown error a 500 with a request ID. Never return a stack trace in the body.
Testing seams
A seam is a place where you can change behaviour without editing the code under test. An interface parameter is the usual seam. Put it where the outside world begins: the database, the payment provider, the clock.
Prefer a fake to a mock. A fake is a small working implementation, such as an in-memory store. A mock asserts that a method was called, which couples the test to the implementation. Ledgerline's fake PSP drops, duplicates and delays callbacks on purpose.
SDE-2 depth
- A plugin architecture is a registry of constructors keyed by name, behind one interface. Validate the name at startup.
- Hexagonal architecture keeps the domain in the middle and pushes HTTP, SQL and vendor SDKs to adapters. The test: can the core run with no database and no network?
- A dependency cycle means two packages need each other. Break it by moving the shared type down, defining the interface on the consumer side, or using an event.
- API evolution means adding optional fields, versioning when you must, and deprecating in public. Never change the meaning of a field in place.
Resources
Learn
- Effective Go: interfaces, embedding and composition from the maintainers.
- Object types in TypeScript: the structural typing model behind TS interfaces.
- SOLID principles: one worked example per principle, close to interview depth.
- Inversion of control and dependency injection: the article that named the pattern.
Practice
- Strategy pattern: redraw the diagram, then replace one
ifin your code with it. - The pattern catalogue: name the pattern you already wrote, do not collect them.
- Mocks aren't stubs: read before you write another mock.
Long-form
- Errors are values: the mindset behind Go error handling.
- Working with errors in Go 1.13: wrapping,
errors.Isanderrors.As. - Hexagonal architecture: the original ports-and-adapters note, for after the offer.
Questions to answer out loud
SDE-1:
- State the four pillars in one sentence each, with an example from your code.
- What is the difference between encapsulation and abstraction?
- When would you choose inheritance over composition?
- What makes an interface good, and when is one unnecessary?
- Explain SOLID, then name one violation you shipped.
- Strategy versus factory: what problem does each solve?
- Why does constructor injection beat a global client?
- How do you return and inspect an error in Go?
- Why is a fake better than a mock for the payment provider?
SDE-2:
- How would you design a plugin system that loads implementations by name?
- How do hexagonal boundaries change your package layout?
- How do you break a dependency cycle between two packages?
- How do you evolve a published interface without breaking callers?
How this shows up in your projects
- Ledgerline defines a
PSPConnectorinterface with a real adapter and a chaos fake that drops, duplicates and delays callbacks. - Its retry policy is a strategy: fixed delay in tests, exponential with jitter in production.
- Shiplane's
Deployerhides blue/green versus rolling, and both projects wire concrete types inmain.
Common mistakes
- Give one concrete example per principle, from your own repo.
- Keep interfaces to one or two methods until a real need appears.
- Inject the clock, the payment provider and the database.
- Admit the SOLID violation you shipped, then describe the fix.