- Chapter
- Chapter 1 — What Is Software Design?
- Prerequisites
- Comfortable with Java classes, interfaces and packages (Book 1)
- You will be able to
- Define software design precisely, place a decision at the architecture, HLD, LLD or implementation level, and produce both views for a feature
- Covers
- What is software design? · Design vs implementation · Design vs architecture · Low-level design · High-level design
On this page
- 1. Concept / Definition
- 2. Why It Exists
- 3. Core Concepts
- 4. How It Works Internally — Design as a Process
- 4A. Deep Dive — How Much Design Up Front?
- 5. APIs / Features / Syntax — Java Features That Express Design
- 6. Practical Examples
- 7. Common Problems & Mistakes
- 8. Performance & Production Considerations
- 9. Architecture & Design Considerations
- 10. Interview Questions & Answers
1Concept / Definition
Software design is the activity — and the result — of deciding how a software system is organised into parts, what each part is responsible for, how parts interact and depend on each other, and which details each part hides. The outcome of design is a structure: modules, classes, interfaces, data models and the relationships between them.

Three properties characterise design decisions:
| Property | Meaning | Example |
|---|---|---|
| Structural | Decides parts and relationships, not individual statements | "Pricing rules live in a PricingPolicy separate from Order" |
| Consequential | Affects many future changes, not just the current one | Choosing to pass Money objects instead of double everywhere |
| Constraining | Limits what is easy or hard to do later | Making Order immutable forces state changes through new instances |
2Why It Exists
In the 1960s–70s software grew beyond what one person could hold in their head, and the "software crisis" — late, buggy, unmaintainable projects — made clear that writing correct statements was not enough. Structured design (Stevens, Myers and Constantine, 1974), information hiding (Parnas, 1972) and later object-oriented design (Booch, Meyer, Gamma et al.) introduced explicit ways of deciding structure. Agile practices (from 2001) did not abolish design; they made it continuous and evolutionary — "just enough" upfront, refined through refactoring.
3Core Concepts
3.1 What is software design?
Design answers four recurring questions for any piece of software:
| Question | Design output | Java expression |
|---|---|---|
| What are the parts? | Modules, components, classes | Packages, classes, records, JPMS modules |
| What is each part responsible for? | Responsibilities, invariants | Class purpose, method set, validation |
| How do parts collaborate? | Interfaces, messages, data flow | Interfaces, method signatures, events |
| What does each part hide? | Encapsulated decisions | private members, non-exported packages |
Design operates under forces: functional requirements, quality requirements (performance, security), constraints (team, technology, deadline) and expected change. Good design balances these forces; there is no context-free "correct" design (Topic 1.4).
3.2 Design vs implementation

Implementation is the realisation of a design in code: algorithms, idioms, error handling, library usage. The two are intertwined but answer different questions.
// Design decision: price calculation is a separate, replaceable policy
public interface PricingPolicy {
Money priceFor(Cart cart, Customer customer);
}
// Implementation decision: how this particular policy computes the price
public final class TieredPricingPolicy implements PricingPolicy {
@Override
public Money priceFor(Cart cart, Customer customer) {
Money subtotal = cart.lines().stream()
.map(CartLine::total)
.reduce(Money.ZERO, Money::plus);
return customer.isGold() ? subtotal.times(0.9) : subtotal;
}
}| Aspect | Design | Implementation |
|---|---|---|
| Scope of impact | Many classes, many future changes | One method or class |
| Cost to change later | Higher | Lower |
| Typical review question | "Is this the right place for this responsibility?" | "Is this code correct and clear?" |
| Artefacts | Interfaces, types, package structure, diagrams, ADRs | Method bodies, tests |
A correct implementation can sit inside a poor design (logic duplicated across five services), and a good design can have buggy implementations (fixed locally). Code reviews should examine both.
3.3 Design vs architecture
Architecture is the subset of design decisions that are significant — expensive to change, cross-cutting, or tied to key quality attributes. Grady Booch put it as: architecture represents the significant design decisions, where significance is measured by cost of change.

| Decision | Level | Why |
|---|---|---|
| Split the system into order, billing and shipping services | Architecture | Affects deployment, teams, data ownership |
| Use asynchronous events between services | Architecture | Affects consistency, availability, tooling |
OrderService exposes place(PlaceOrder) and publishes OrderPlaced | High-level design | Component interface and interaction |
Order aggregates OrderLines and enforces a maximum quantity | Low-level design | Class responsibility and invariant |
Use a stream().reduce() to compute the total | Implementation | Local coding choice |
The boundary is fuzzy and context-dependent: choosing a date/time type is implementation in one class, but architecture if it defines the time model across fifty services.
3.4 Low-level design
Low-level design (LLD) specifies the inside of a component: classes, their fields and methods, relationships (association, composition, inheritance), algorithms and data structures, and the patterns applied. It is what interviewers mean by "design a parking lot" or "design an LRU cache".
// LLD sketch: Order aggregate with an invariant and intention-revealing methods
public final class Order {
private static final int MAX_LINES = 50;
private final OrderId id;
private final List<OrderLine> lines = new ArrayList<>();
private OrderStatus status = OrderStatus.DRAFT;
public Order(OrderId id) { this.id = Objects.requireNonNull(id); }
public void addLine(Product product, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Order not editable");
}
if (lines.size() == MAX_LINES) throw new IllegalStateException("Too many lines");
lines.add(new OrderLine(product, quantity));
}
public Money total() {
return lines.stream().map(OrderLine::total).reduce(Money.ZERO, Money::plus);
}
public void submit() {
if (lines.isEmpty()) throw new IllegalStateException("Empty order");
status = OrderStatus.SUBMITTED;
}
public List<OrderLine> lines() { return List.copyOf(lines); }
}| LLD element | Typical notation |
|---|---|
| Classes and relationships | UML class diagram |
| Interaction over time | UML sequence diagram |
| Lifecycle of an object | State diagram |
| Invariants and contracts | Javadoc, assertions, types |
3.5 High-level design
High-level design (HLD) describes a system or subsystem in terms of components (services, modules, databases, external systems), their interfaces, and data and control flow between them. It sits between architecture and LLD and is what "system design" interviews focus on at component level.

| HLD element | Question answered |
|---|---|
| Component diagram | Which components exist and who talks to whom? |
| Interfaces / API definitions | What operations and data cross each boundary? |
| Data ownership | Which component owns which data? |
| Key flows | How does a request move through components? |
| Non-functional mapping | Where are caching, retries, security applied? |
4How It Works Internally — Design as a Process
1. Understand forces: requirements, quality goals, constraints, expected change.
2. Identify concepts from the domain (nouns, verbs, rules) and candidate responsibilities.
3. Assign responsibilities to components (HLD) and then classes (LLD) — keeping related responsibilities together and volatile decisions hidden.
4. Define interfaces between the parts, including contracts and error behaviour.
5. Validate with scenarios ("what if tax rules change?"), prototypes and tests.
6. Implement and refine: refactoring continuously improves the design as understanding grows.
Design proceeds top-down (decomposing a system) and bottom-up (extracting abstractions from working code) at the same time. Test-driven development is one technique that drives LLD from the caller's perspective.
4ADeep Dive — How Much Design Up Front?
| Approach | When it fits | Risk |
|---|---|---|
| Big design up front (BDUF) | Safety-critical, contractual, hard-to-change hardware interfaces | Designs for imagined requirements; late feedback |
| Emergent design (TDD + refactoring only) | Small teams, uncertain domain, easy deployment | Architectural decisions made accidentally |
| Just enough design up front | Most product teams | Requires judgement about what is "significant" |
A practical rule: decide up front what is expensive to change (service boundaries, data ownership, public APIs, persistence model) and let the rest emerge through TDD and refactoring. Record significant decisions in ADRs (Book 4, Topic 10.7).
4A.1 Design documentation that stays useful
| Artefact | Level | Keep it alive by |
|---|---|---|
| C4 context/container diagrams | Architecture/HLD | Generating from code or reviewing each quarter |
| Module/package diagrams | HLD/LLD | ArchUnit/Spring Modulith generation |
| ADRs | All levels | Writing at decision time, never editing history |
| Javadoc on public types | LLD | Treating it as part of the contract |
5APIs / Features / Syntax — Java Features That Express Design
| Java feature | Design intent it expresses |
|---|---|
interface, sealed interface | Abstractions and closed sets of variants |
record | Immutable value/data carriers |
private, package-private | Encapsulation and information hiding |
JPMS module-info.java (exports, requires) | Module boundaries enforced by the compiler |
final classes and fields | Prevent unintended extension/mutation |
| Generics | Reusable, type-safe abstractions |
Pattern matching switch | Exhaustive handling of sealed hierarchies |
// A sealed hierarchy documents a design decision: exactly these payment types exist
public sealed interface Payment permits CardPayment, BankTransfer, Voucher { }
public record CardPayment(String token, Money amount) implements Payment { }
public record BankTransfer(String iban, Money amount) implements Payment { }
public record Voucher(String code, Money amount) implements Payment { }
Money fee(Payment p) {
return switch (p) { // compiler checks exhaustiveness
case CardPayment c -> c.amount().times(0.015);
case BankTransfer b -> Money.of("0.20");
case Voucher v -> Money.ZERO;
};
}6Practical Examples
6.1 One feature, four levels
| Level | "Customers can pay by voucher" |
|---|---|
| Architecture | Vouchers issued by a separate Promotions service; payments service calls it |
| HLD | PaymentService → VoucherClient.redeem(code); PaymentCompleted event |
| LLD | Voucher record in sealed Payment hierarchy; VoucherRedemption handles idempotency |
| Implementation | HTTP client with timeout; retry once on 503; map error codes to exceptions |
6.2 Recognising a hidden design decision in code
// Before: design decision (tax rate) buried in implementation, repeated in 4 places
double total = subtotal * 1.19;
// After: decision made explicit and located in one place
public interface TaxPolicy { Money taxOn(Money net, Region region); }
Money total = net.plus(taxPolicy.taxOn(net, customer.region()));6.3 LLD interview template
| Step | What to produce |
|---|---|
| 1. Clarify | Use cases, constraints, scale, out of scope |
| 2. Entities | Core classes/records and their responsibilities |
| 3. Relationships | Composition, association, interfaces |
| 4. Key flows | Sequence of calls for main use cases |
| 5. Extension points | Where variation is expected (Strategy, State…) |
| 6. Edge cases | Concurrency, invalid input, failure |
6.4 HLD review checklist
- Every component has a clear responsibility and owner.
- Every piece of data has exactly one owning component.
- Interfaces are documented with error behaviour and versioning.
- Main flows are shown as sequences, including failure paths.
- Quality requirements (latency, availability) are mapped to mechanisms.
7Common Problems & Mistakes
- Equating design with diagrams — diagrams drift; code is the truth.
- No design at all ("we're agile") — structure emerges accidentally and expensively.
- Designing for imagined futures — speculative generality (Topic 1.4).
- Mixing levels in one discussion — debating naming while service boundaries are undecided.
- Architecture by technology choice — picking frameworks before understanding responsibilities.
- Treating LLD as "just coding" — skipping responsibility assignment inside components.
8Performance & Production Considerations
| Design choice | Production effect |
|---|---|
| Component boundaries | Number of network hops, latency budget |
| Data ownership | Query patterns, need for replication/caching |
| Object model (mutable vs immutable) | Thread safety, GC pressure |
| Interface granularity | Chatty vs coarse calls, payload sizes |
Performance-critical paths are design decisions too: batching, caching and asynchronous processing are hard to retrofit if interfaces assume one item per synchronous call.
9Architecture & Design Considerations
- Architecture is design with a high cost of change — invest review effort accordingly.
- Keep levels consistent: HLD component boundaries should be visible in LLD packages and modules.
- Make design explicit in code: types, modules and names should reveal the intended structure.
- Evolve deliberately: refactor as understanding grows; record significant decisions.
10Interview Questions & Answers
What is software design?
Software design is the set of decisions about how a system is structured: how it is divided into parts, what each part is responsible for, how the parts interact and depend on each other, and what each part hides from the others. These decisions exist whether or not anyone makes them deliberately; code that grows without design still has a structure, just an accidental one. Good design makes the likely changes cheap. For example, hiding the payment provider behind an interface means switching providers touches one class. Design happens at every level, from method signatures to service boundaries.
What is the difference between design and implementation?
Design decides what the parts of a system are and how they relate: responsibilities, interfaces, dependencies and data ownership. Implementation realises those decisions in concrete code: algorithms, data structures, error handling and the lines of each method. The difference is mostly about reach. A design decision, such as splitting order and payment logic into separate modules with a defined interface, affects many future changes and is expensive to reverse. An implementation decision, such as using a loop or a stream inside one method, affects only local code and can be changed cheaply. The line between them is blurry, but the cost of change is a good guide.
What is the difference between high-level and low-level design?
High-level design describes a system in terms of its major components: services or modules, their responsibilities, the interfaces between them, the data each owns and how requests and data flow through the system. It answers questions such as "which component handles payments and how does checkout call it?" Low-level design zooms into one component: its classes and interfaces, their methods and relationships, the algorithms and the data structures. It answers questions such as "how is a discount calculated and which class owns that rule?" Both are needed, and they must stay consistent; a clean high-level diagram is worthless if the code inside ignores it.
How does architecture differ from design?
Architecture is the subset of design decisions that are significant for the system as a whole. A decision is architectural if it is expensive to change later, affects many parts of the system, or determines key quality attributes such as performance, availability or security. Choosing between a modular monolith and microservices, deciding which service owns customer data or selecting an event-driven integration style are architectural; naming a class or choosing a collection type are design but not architecture. The distinction matters because architectural decisions deserve more analysis, wider review and written records such as ADRs, while ordinary design decisions can evolve freely.
How much design should be done up front in an agile team?
Do enough design up front to settle the decisions that are expensive to reverse, and let the rest emerge. Before building, agree on module or service boundaries, data ownership, public APIs and contracts with other teams, and the approach to key quality attributes such as security and scalability. Inside those boundaries, let detailed design evolve through test-driven development and continuous refactoring, guided by real requirements rather than speculation. Record significant decisions in ADRs so they can be revisited deliberately. The aim is neither a big design phase nor no design at all, but just enough design at the right time.
How do Java language features support design?
Modern Java gives designers precise tools. Interfaces define abstractions that hide implementations. Sealed interfaces and classes model a closed set of variants, such as the possible payment results, and pattern-matching switches over them are checked for exhaustiveness by the compiler. Records model immutable values with value-based equality, making value objects such as Money or OrderId cheap to write. Access modifiers, package structure and the module system enforce encapsulation, so internal classes cannot be used from outside. Using these features deliberately makes design intentions visible in code and lets the compiler catch violations that would otherwise be found in reviews or production.
How do you keep HLD and LLD consistent over time?
Make the high-level structure visible in the code itself. Map each component in the high-level design to a module, package or build module with the same name, so the code mirrors the diagram. Enforce the intended dependencies automatically, using the Java module system, ArchUnit tests or Spring Modulith verification, so a violation fails the build rather than drifting silently. Generate component diagrams from the code where possible, instead of maintaining them by hand. Finally, review changes to module interfaces and dependencies more carefully than ordinary changes, for example by requiring an architect's approval or an ADR for new cross-module dependencies.
How would you run a design review for a new capability?
Prepare the review around the forces and goals first: the business capability, key quality attributes such as latency and availability, and constraints such as compliance or existing systems. Then review the high-level design: components, data ownership, interfaces, integration style and how failures are handled end to end. Spot-check the low-level design of the core domain classes, where the most important rules live. Identify the decisions that are hard to reverse, discuss alternatives and their consequences, and record them in ADRs. Keep the review time-boxed, invite the people who will build and run the system, and agree on concrete follow-up actions with owners.