SDE-1 All-In Guide
CS Fundamentals: Track B

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

AreaSDE-1 mustSDE-2 later
PillarsEncapsulation, abstraction, inheritance, polymorphismTrade-offs between them
SOLIDName each principle and apply itWhen to break one on purpose
CompositionPrefer it, with a reasonEmbedding edge cases
InterfacesSmall, one job, defined by the callerSegregation across packages
PatternsStrategy, factory, observer, adapterPlugin registries
Dependency injectionConstructor injection, wired in mainContainers and scopes
Errors and testingValues, wrapping, fakes behind interfacesContract tests
ArchitecturePackages that depend one wayHexagonal 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

TopicWhat to be able to explainLevel
Encapsulation and abstractionHide state, name the conceptSDE-1 must
Inheritance vs compositionWhat each buys and couplesSDE-1 must
PolymorphismOne call site, several implementationsSDE-1 must
InterfacesSmall contracts owned by the consumerSDE-1 must
SOLIDOne example per letterSDE-1 must
StrategySwap an algorithm without editing the callerSDE-1 must
Factory, observer, adapterWhat each decouplesSDE-1 nice
Dependency injectionPass dependencies inSDE-1 must
Errors as designFailure is part of the contractSDE-1 must
Testing seamsFake the boundary, not the internalsSDE-1 must
Hexagonal and pluginsPorts, adapters, and a pure coreSDE-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

PrinciplePlain meaningExample you can point at
Single responsibilityOne reason to changeOutboxWorker delivers; it does not compute balances
Open for extensionAdd a case without editing the coreA new PSPConnector needs no change in Ledger
Liskov substitutionA subtype must keep the promiseA fake PSP still returns a charge result
Interface segregationNo caller depends on methods it never usesSplit Reader and Writer, not one Store
Dependency inversionDepend on the contract, not the driverLedger 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

Practice

Long-form

Questions to answer out loud

SDE-1:

  1. State the four pillars in one sentence each, with an example from your code.
  2. What is the difference between encapsulation and abstraction?
  3. When would you choose inheritance over composition?
  4. What makes an interface good, and when is one unnecessary?
  5. Explain SOLID, then name one violation you shipped.
  6. Strategy versus factory: what problem does each solve?
  7. Why does constructor injection beat a global client?
  8. How do you return and inspect an error in Go?
  9. Why is a fake better than a mock for the payment provider?

SDE-2:

  1. How would you design a plugin system that loads implementations by name?
  2. How do hexagonal boundaries change your package layout?
  3. How do you break a dependency cycle between two packages?
  4. How do you evolve a published interface without breaking callers?

How this shows up in your projects

  • Ledgerline defines a PSPConnector interface 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 Deployer hides blue/green versus rolling, and both projects wire concrete types in main.

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.

On this page