Software Design and Architecture Roadmap

Software design is how you shape code inside a component: classes, functions, names and the boundaries between them. Architecture is the larger shape: how a system is split into parts, how they communicate and where data lives. Both decide how cheaply a system can change, which is what most of an engineer's working life is spent doing.

This roadmap starts with programming paradigms and clean code, moves through object-oriented principles, SOLID and design patterns, then covers enterprise patterns, domain-driven design and architectural styles from monoliths to event-driven and serverless systems. Each topic explains the idea, shows a short example where code helps, and ends with an exercise. Core topics are expected in interviews, recommended topics come up in most real jobs, and optional ones are good to recognise.

By the end you will be able to write maintainable code, review a design with clear reasoning, and discuss architectural trade-offs in interviews. For the large-scale distributed side, pair this with the System Design Roadmap, and practise designs in system design practice.

Level
Beginner to advanced
Time
14–20 weeks at about 8 hours a week, alongside regular coding practice
Scope
54 topics in 9 stages
Before you start
Confident programming in one object-oriented language such as Java Roadmap, Python Roadmap, C# or TypeScript Roadmap; Writing and running unit tests in that language; Basic Git and GitHub Roadmap and some experience of working in a shared codebase; For the later stages, a working knowledge of SQL Roadmap and HTTP APIs

What you will be able to do

  • Write clean, readable code and refactor messy code safely in small steps.
  • Apply encapsulation, polymorphism, composition and the SOLID principles, and explain when each helps.
  • Recognise and implement the most common design patterns and avoid overusing them.
  • Organise business logic with transaction scripts, domain models, repositories and use cases.
  • Model a complex domain with bounded contexts, entities, value objects and aggregates.
  • Compare monolithic, layered, microservice, event-driven and serverless architectures and justify a choice.
  • Answer design and architecture interview questions with clear trade-offs.

The roadmap at a glance

  1. Stage 1Programming Paradigms and First PrinciplesBeginner · 1–2 weeks · 3 topics
  2. Stage 2Object-Oriented Design in DepthBeginner · 1–2 weeks · 5 topics
  3. Stage 3Clean CodeBeginner · 2 weeks · 7 topics
  4. Stage 4Design PrinciplesIntermediate · 2 weeks · 7 topics
  5. Stage 5Design PatternsIntermediate · 2–3 weeks · 5 topics
  6. Stage 6Enterprise Application PatternsIntermediate · 2 weeks · 7 topics
  7. Stage 7Domain-Driven DesignAdvanced · 2–3 weeks · 7 topics
  8. Stage 8Architectural PrinciplesAdvanced · 2 weeks · 5 topics
  9. Stage 9Architectural StylesAdvanced · 2–3 weeks · 8 topics

Stage 1 Programming Paradigms and First Principles

Beginner1–2 weeks3 topics

Goal. Explain the main programming paradigms and write small programs that keep data and behaviour under control.

Programming Paradigms Core

A paradigm is a set of habits about how to organise a program: what the basic unit is, where state lives and how control flows. Three matter most for design work.

  • Structured programming builds logic from sequence, selection and loops, with no arbitrary jumps.
  • Object-oriented programming bundles state with the operations that change it.
  • Functional programming favours pure functions and immutable values.

Each paradigm takes something away from you: goto, direct access to another object's data, or mutation. Those restrictions are what make code easier to reason about. Modern languages such as Kotlin, C#, TypeScript and Python mix all three, so the useful skill is knowing which style fits which part of a system, not picking a camp.

Practice. Solve the same small problem, such as totalling an order with discounts, once in each of the three styles and compare them.

Object Oriented Programming Core

Object-oriented programming models a program as objects that hold state and expose behaviour through methods. Instead of passing a record around and letting any function change it, you ask the object to do the work, and it protects its own rules.

The four ideas usually listed are encapsulation, abstraction, inheritance and polymorphism. The design payoff comes mostly from encapsulation and polymorphism: they let you change how something works without changing everyone who uses it.

OOP is easy to do badly. Classes that are just bags of getters and setters, deep inheritance trees and giant manager classes are common. Good OOP is about assigning responsibilities: which object knows this data, and therefore which object should make this decision.

Practice. Model a library loan system with Book, Member and Loan classes, deciding which class owns the rule that a member can hold at most five books.

Pure Functions Core

Give a pure function identical arguments and it hands back an identical result every time, and it leaves no trace behind it: no writing to a database, no logging, no changing its arguments, no reading the clock or a global. Because of that, you can test it with plain inputs and outputs, cache its results, run it in parallel and move it around safely.

# impure: depends on hidden state
def price_with_tax(amount):
    return amount * settings.TAX_RATE

# pure: everything it needs is a parameter
def price_with_tax(amount, tax_rate):
    return amount * tax_rate

Functional programming is built on this idea. A popular modern layout is a functional core, imperative shell: business decisions are pure, and a thin outer layer talks to databases, HTTP and the clock.

Practice. Find three functions in your code that read the current time or a global setting, and change them to take that value as a parameter.

Stage 2 Object-Oriented Design in Depth

Beginner1–2 weeks5 topics

Goal. Use encapsulation, abstraction, interfaces and polymorphism deliberately, and choose composition when inheritance would be fragile.

Encapsulation Core

Encapsulation means an object guards its own data and the rules around it. Other code can ask it to do things, but cannot reach in and put it into an invalid state.

class Account {
  private balance = 0;
  withdraw(amount: number) {
    if (amount <= 0 || amount > this.balance) throw new Error("invalid");
    this.balance -= amount;
  }
  get currentBalance() { return this.balance; }
}

Private fields alone are not encapsulation. A class with a public setter for every field still lets callers break its invariants. Ask what operations the outside world needs, expose those, and keep the representation hidden so you can change it later.

Scope and visibility support it: make members private by default, declare variables in the smallest block, and widen access only when a caller needs it. Everything public is a promise.

Practice. Take a class with public setters and replace them with intention-revealing methods that enforce its rules.

Inheritance Core

Inheritance lets a subclass reuse and specialise the behaviour of a parent class. It expresses an is-a relationship: a SavingsAccount is an Account.

It is the tightest form of coupling in OOP. A subclass depends on the parent's internals, so a harmless-looking change in the parent can break every child, which is known as the fragile base class problem. Overriding methods also makes behaviour hard to trace.

Use it when the subtype really can stand in for the parent everywhere, the hierarchy is shallow, and the parent was designed to be extended. Many languages now make classes closed by default or offer sealed or final for this reason. Otherwise prefer composition.

Practice. Find an inheritance chain three or more levels deep and sketch how it would look using composition instead.

Polymorphism Core

Polymorphism lets one piece of code work with many types through a shared interface, with each type supplying its own behaviour. It is the main tool for removing long if or switch chains on type codes.

interface Shape { double area(); }
record Circle(double r) implements Shape {
  public double area() { return Math.PI * r * r; }
}
record Square(double side) implements Shape {
  public double area() { return side * side; }
}
double total(List<Shape> shapes) {
  return shapes.stream().mapToDouble(Shape::area).sum();
}

Adding a triangle needs a new class, not edits to total. Other forms exist too: overloading, generics (parametric polymorphism) and duck typing in dynamic languages.

Practice. Replace a switch on a type field with polymorphic classes and add a new variant without touching the calling code.

Interfaces, Abstract Classes and Concrete Classes Core

A concrete class can be instantiated and has full behaviour. An abstract class cannot be instantiated; it shares state and some implementation while leaving gaps for subclasses. An interface is a pure contract: method signatures that any class can promise to fulfil.

Rules of thumb that interviewers like:

  • Depend on interfaces at boundaries, because they carry no implementation baggage and a class can implement many.
  • Use an abstract class when several related classes share real state or a fixed algorithm with variable steps.
  • Keep concrete classes for the actual work, and do not add an interface for a class with one implementation and no test or boundary need.

Java and C# interfaces can now hold default methods, which blurs the line, but the intent remains the same.

Class variants signal intent too: records or data classes for immutable values, enums for fixed options, sealed types for closed sets of subtypes.

Practice. Design a notification sender with an interface, one abstract base for shared retry logic and two concrete senders.

Composition over Inheritance Core

Composition builds behaviour by holding other objects and delegating to them, rather than inheriting from a parent. It gives you looser coupling, behaviour you can swap at runtime and smaller classes that each do one thing.

class ReportService {
  constructor(
    private readonly source: DataSource,
    private readonly formatter: Formatter,
  ) {}
  build() { return this.formatter.format(this.source.load()); }
}

With inheritance you would need CsvDatabaseReport, PdfDatabaseReport, CsvApiReport and so on, one class per combination. With composition you combine one source and one formatter. Inheritance is still fine for genuine subtypes; the guideline means reaching for composition first.

Practice. Refactor a class hierarchy that multiplies by two dimensions into a composed design and count the classes you remove.

Stage 3 Clean Code

Beginner2 weeks7 topics

Goal. Write code that another engineer can read, change and test without asking you what it does.

Meaningful Names Core

A good name tells the reader what something is for, so they do not need to read its body. Among the clean code principles, naming comes first: names are the cheapest documentation you have.

  • Name variables after their meaning, not their type: expiresAt, not date2.
  • Functions are verbs that say what they achieve: calculateRefund, sendInvoice.
  • Booleans read as questions: isActive, hasAccess.
  • Use the words the business uses, and one word per concept across the codebase.
  • Length should match scope: i is fine in a three-line loop, not in a module-level variable.

If you cannot find a clear name, the thing often does too much and should be split.

Practice. Rename every vague identifier in one file, such as data, info, tmp or handle, to something specific.

Keep it Small Core

Small functions and classes are easier to name, test and reuse. A function should do one thing at one level of abstraction: either orchestrate steps or perform a detail, not both.

def place_order(cart, customer):
    validate(cart)
    total = price(cart, customer)
    payment = charge(customer, total)
    return save_order(cart, payment)

There is no magic line count, but if a function needs comments to separate its sections, those sections are probably functions. The same applies to classes with many unrelated fields. Do not go to the other extreme of one-line functions that scatter a simple idea across ten files.

Practice. Split a function longer than 40 lines into steps that each fit on one screen and read like a list of intentions.

Avoiding Nulls and Booleans as Arguments Recommended

Passing null or a bare boolean into a function hides meaning and multiplies paths through the code.

  • A call like render(page, true, false) tells the reader nothing. Split it into two functions, use named arguments, or pass an enum such as Layout.Compact.
  • A boolean flag often means the function does two different jobs; make two functions.
  • Accepting null forces every callee to check. Use an optional type, a default value, an empty collection, or a null object that does nothing safely.

Languages such as Kotlin, Swift, TypeScript with strict null checks and C# with nullable reference types let the compiler track nullability. Turn those features on.

Practice. Find three functions with boolean parameters and replace each with clearer alternatives such as separate functions or an enum.

Minimize Cyclomatic Complexity Recommended

Cyclomatic complexity counts the independent paths through a function; roughly, one plus the number of decision points such as if, loops, case and boolean operators. Each path needs a test, and each is a place for bugs to hide.

Ways to bring it down:

  • Use guard clauses that return early instead of nested if blocks.
  • Replace type switches with polymorphism or a lookup table.
  • Extract complex conditions into well-named predicate functions.
  • Split functions that handle several cases into one function per case.

Linters can report the number and fail a build above a threshold. Treat it as a signal to look, not a score to game.

Practice. Run a complexity linter on a project and halve the score of the worst function without changing its behaviour.

Be Consistent: Style and Constructs Core

Once a reader learns how one part of a codebase works, they should be able to predict the rest: one error-handling approach, one test naming scheme, one folder pattern, one word per business concept. Follow the existing convention even when you prefer another, and propose codebase-wide changes separately.

Indentation and code style should be automated, not debated. Adopt your language's community style, such as PEP 8 or gofmt, run a formatter such as Prettier, Black or rustfmt on save and in CI, and add a linter. Deep indentation is a design smell, not a layout problem.

Use correct constructs as well: enums instead of magic strings, for-each loops instead of index arithmetic, with, using or try-with-resources for clean-up, and immutable declarations by default.

Practice. List five conventions in a codebase you know, then find and fix one place that breaks each of them.

Keep it simple and refactor often Core

Write the simplest code that solves today's problem clearly, then improve its structure as you learn more. Refactoring means changing structure without changing behaviour, in small steps, with tests passing after each one.

Common refactorings are worth knowing by name: extract function, inline function, rename, move method, extract class, replace conditional with polymorphism and introduce parameter object. Modern IDEs automate most of them safely.

Refactor as part of normal work: when you add a feature and the code fights you, first make the change easy, then make the easy change. Avoid long-lived refactoring branches; they drift from main and are painful to merge. Keep refactoring commits separate from behaviour changes so reviewers can check each quickly.

Practice. Pick a messy function covered by tests and apply five named refactorings, committing after each one.

Tests should be fast and independent Core

Tests are what make refactoring and design change safe, but only if people run them constantly. That requires speed and independence.

  • Fast: unit tests should run in milliseconds. Keep databases, networks and sleeps out of them by testing pure logic directly and putting I/O behind interfaces.
  • Independent: each test sets up its own data and can run alone or in any order. Shared mutable fixtures cause failures that only appear in a full run.
  • Repeatable: control time, randomness and IDs through parameters or fakes.

Testability is a design signal. If a class is painful to test, it usually has too many dependencies or hides its I/O. Fix the design rather than reaching for heavier mocking.

Practice. Find your slowest unit test, work out why it is slow, and make it run in under 50 milliseconds.

Stage 4 Design Principles

Intermediate2 weeks7 topics

Goal. Apply coupling, cohesion, SOLID and related principles to judge and improve a design, and explain the trade-offs in an interview.

Coupling and Cohesion Core

These two ideas sit under almost every other principle.

  • Coupling is how much one module depends on another. High coupling means a change in one place forces changes elsewhere.
  • Cohesion is how closely the contents of a module belong together. High cohesion means a module has one clear purpose and its parts change for the same reasons.

Aim for low coupling and high cohesion. Coupling comes in degrees: depending on another module's private data is worse than calling its public methods, which is worse than exchanging simple messages. Some coupling is unavoidable; the goal is to make it explicit, narrow and pointed at stable things. When a single feature change touches ten files across five folders, you have high coupling and low cohesion.

Practice. Look at your last five pull requests and list which modules changed together; decide whether they belong in one module.

DRY, KISS and YAGNI Core

Three short principles that guard against over- and under-engineering.

  • DRY (do not repeat yourself): every piece of knowledge, such as a business rule or a format, should live in one place. It is about knowledge, not text; two similar-looking blocks that change for different reasons are not duplication.
  • KISS (keep it simple): prefer the plainest solution that works.
  • YAGNI (you are not going to need it): do not build features, options or extension points for imagined future needs.

A common senior answer: a little duplication is cheaper than the wrong abstraction. Wait until you see the third case before generalising, and make sure the cases really share a reason to change.

Practice. Find a shared helper with many flags added for different callers and decide whether to split it back into separate functions.

SOLID Principles: Single Responsibility and Open-Closed Core

The SOLID Principles are five guidelines for object-oriented design. The first two:

  • Single responsibility: a module should have one reason to change, meaning it should answer to one group of people or one concern. A class that calculates pay, formats a report and saves to the database will change for finance, design and database reasons.
  • Open-closed: you should be able to add behaviour by adding new code rather than editing working code. Polymorphism, strategies and plug-in points make this possible.

Do not apply open-closed everywhere in advance. Add extension points where change has already happened or is clearly expected, otherwise you violate YAGNI.

Practice. Split a class that mixes calculation, formatting and persistence into three, and add a new output format without editing the calculation.

SOLID Principles: Liskov, Interface Segregation and Dependency Inversion Core

  • Liskov substitution: a subtype must work anywhere its parent is expected, without surprising callers. A ReadOnlyList that throws on add breaks code written against List. Subtypes should not demand more or promise less.
  • Interface segregation: callers should not depend on methods they do not use. Prefer several small, role-based interfaces, such as Reader and Writer, over one wide one.
  • Dependency inversion: high-level policy should not depend on low-level details; both should depend on abstractions owned by the policy side. The order service defines an OrderStore interface, and the SQL adapter implements it.

Dependency inversion is the principle behind hexagonal and clean architecture, so expect follow-up questions on it.

Practice. Find a business class that imports a database or HTTP library directly and invert the dependency behind an interface it owns.

Encapsulate What Varies and Program Against Abstractions Core

Encapsulate what varies: find the parts most likely to change, such as tax rules, payment providers or file formats, and put each behind a stable interface. Then program against abstractions: declare parameters and fields as the narrowest type that does the job, such as Clock rather than the system time, or PaymentGateway rather than one vendor's SDK.

class Checkout {
  private final ShippingRule shipping;      // an interface
  Checkout(ShippingRule shipping) { this.shipping = shipping; }
}

Collaborators arrive through the constructor, which is dependency injection. That is one form of the Hollywood Principle, do not call us, we will call you: frameworks, test runners and event buses call your code, and one composition root wires objects together. Add an interface where there is an I/O boundary or several real implementations, not for every class.

Practice. Change three function signatures in your code to accept the narrowest interface that still works.

Law of Demeter Recommended

The Law of Demeter, or principle of least knowledge, says a method should only talk to its immediate collaborators: its own fields, its parameters and objects it creates. It should not reach through them to strangers.

// violates: knows the internal structure of customer
order.getCustomer().getWallet().debit(total);

// better: ask the direct collaborator
order.chargeCustomer(total);

Long chains couple the caller to the shape of several classes, so restructuring any of them breaks it. The rule does not apply to fluent builders or stream pipelines that return the same kind of object, and it does not mean wrapping every getter.

Its close cousin is Tell, Don't Ask: tell an object to act, as in account.withdraw(amount), rather than reading its fields and deciding for it. Rules stay next to the data, which is the cure for anemic models.

Practice. Search a codebase for call chains of three or more getters and refactor two of them.

Command Query Separation Recommended

Command Query Separation says each method should either change state (a command) or return information (a query), but not both. Queries can then be called freely, in any order, without side effects, and commands make their effects obvious.

  • cart.total() is a query: no changes.
  • cart.addItem(item) is a command: returns nothing, or at most an acknowledgement.

Practical exceptions exist, such as stack.pop() or an insert that returns the new ID, and most teams accept them. The idea scales up: CQRS, covered later, applies the same split to whole models and data stores. Keep the two straight in interviews: CQS is about methods, CQRS is about architecture.

Practice. Review one class and split any method that both changes state and returns computed data.

Stage 5 Design Patterns

Intermediate2–3 weeks5 topics

Goal. Recognise and apply the common design patterns, explain the problem each solves and know when one is overkill.

GoF Design Patterns Core

Design patterns are named answers to recurring object-oriented problems. Their value is shared vocabulary; their risk is overuse. Learn each one as a problem, a structure and a trade-off.

The GoF Design Patterns are the 23 patterns catalogued by the so-called Gang of Four, grouped into three families.

  • Creational: how objects are made. Factory method, abstract factory, builder, prototype, singleton.
  • Structural: how objects are assembled. Adapter, bridge, composite, decorator, facade, flyweight, proxy.
  • Behavioural: how objects share work. Chain of responsibility, command, interpreter, iterator, mediator, memento, observer, state, strategy, template method, visitor.

Several are now built into languages: iterator in every for loop, observer in event systems, strategy as a passed function. You do not need all 23 for interviews. Know strategy, observer, decorator, adapter, factory, builder, singleton, facade, command and template method very well.

Practice. Make a one-line summary of each of the 23 patterns in your own words, then mark the ten you would expect to use.

Creational Patterns Core

  • Factory method and simple factory: a method decides which concrete class to create, so callers depend only on the interface.
  • Abstract factory: creates families of related objects, such as all widgets for one UI theme.
  • Builder: assembles a complex object step by step, avoiding constructors with many parameters.
  • Singleton: one instance for the whole process. Widely considered an anti-pattern now, because it is global state that hides dependencies and hurts tests. Let your DI container manage one shared instance instead.
val request = HttpRequest.newBuilder()
    .uri(URI.create(url))
    .timeout(Duration.ofSeconds(5))
    .header("Accept", "application/json")
    .build()

Named arguments and default values in many languages remove much of the need for builders.

Practice. Replace a constructor with six parameters with a builder, then try the same with named arguments and compare.

Structural Patterns Core

Structural patterns describe ways to assemble objects into larger, flexible structures.

  • Adapter: wraps a class with an incompatible interface so it fits the one your code expects. The standard way to integrate a third-party SDK.
  • Decorator: wraps an object with the same interface and adds behaviour, such as caching, logging or retries, without changing it.
  • Facade: one simple entry point in front of a complicated subsystem.
  • Proxy: a stand-in that controls access, such as lazy loading, remote calls or permission checks.
  • Composite: treats a tree of objects and a single object the same way, as in UI components or file systems.

Adapter, decorator and proxy look alike in code. The difference is intent: change the interface, add behaviour, or control access.

Practice. Wrap an HTTP client in a retry decorator and a logging decorator, stacking both without changing the client.

Behavioural Patterns Core

  • Strategy: interchangeable algorithms behind one interface, chosen at runtime.
  • Observer: subscribers are notified when a subject changes; the basis of event listeners and reactive streams.
  • Command: wraps a request as an object so it can be queued, logged or undone.
  • Template method: a base class fixes the steps of an algorithm and subclasses fill in some of them.
  • State: an object changes its behaviour when its internal state changes, replacing big status switches.
  • Chain of responsibility: a request passes along handlers until one deals with it; web middleware works this way.
discounts = {"student": lambda p: p * 0.9, "none": lambda p: p}
final = discounts[customer.kind](price)   # strategy as functions

In languages with first-class functions, many behavioural patterns shrink to passing a function.

Practice. Implement an order status workflow first with a switch statement, then with the state pattern, and compare how easy it is to add a status.

PoSA Patterns Optional

PoSA Patterns come from the Pattern-Oriented Software Architecture series, which catalogued patterns at a larger scale than the class-level ones. The best known:

  • Layers: groups of components at increasing levels of abstraction.
  • Pipes and filters: data flows through a chain of independent processing steps, like a Unix pipeline.
  • Broker: a middle component routes requests between distributed clients and servers.
  • Blackboard pattern: independent specialist components post partial results to a shared store until an answer emerges; once used for speech recognition, now echoed in some multi-agent AI systems.
  • Microkernel: a minimal core with plug-ins.
  • Model-view-controller and presentation-abstraction-control for interactive systems.

You rarely hear the series named at work, but the patterns themselves appear constantly under modern names, such as stream processing for pipes and filters.

Practice. Build a three-step text processing pipeline where each filter is a separate function connected by a queue or generator.

Stage 6 Enterprise Application Patterns

Intermediate2 weeks7 topics

Goal. Organise business logic and data access in a typical back-end application, and explain the trade-offs of each option.

Transaction Script Recommended

Enterprise patterns are recurring designs for data-heavy business applications, and the first question they settle is where business logic lives.

A Transaction Script organises business logic as one procedure per user action: registerUser, placeOrder, cancelBooking. Each script reads data, applies the rules and writes the result, usually inside one database transaction.

def cancel_booking(db, booking_id, now):
    booking = db.fetch_booking(booking_id)
    if booking.starts_at - now < timedelta(hours=24):
        raise TooLateToCancel()
    db.update_status(booking_id, "cancelled")
    db.insert_refund(booking_id, booking.amount)

It is simple, fast to write and perfectly fine for CRUD-heavy or small apps. It degrades when rules grow: logic gets duplicated across scripts and nothing enforces invariants in one place. That is the point where a domain model starts to pay off.

Practice. Write three transaction scripts for a booking system and note which rules you had to repeat.

Domain Models Core

A domain model represents the business as objects that hold both data and the rules that govern it. Instead of a script checking whether a booking can be cancelled, the Booking object has a cancel method that enforces the rule itself.

Benefits: rules live in one place, objects cannot be put into invalid states, and the code reads in business terms. Costs: more up-front design, and mapping rich objects to tables needs an ORM or data mapper.

Choose a domain model when business rules are complex and change often, such as in insurance, logistics or finance. Choose transaction scripts when the app mostly moves data in and out. Many systems sensibly mix both, using a domain model only in the complex core.

Practice. Convert the booking transaction scripts from the previous exercise into a Booking class with methods that enforce the rules.

Anemic Models Core

An anemic domain model is a set of classes named after business concepts that contain only fields and getters and setters, while all the behaviour sits in separate service classes. It looks like object-oriented design but is really procedural code with extra ceremony.

The problem is that nothing protects the rules: any service can set order.status = SHIPPED on an unpaid order, and the same checks get copied between services. The fix is to move behaviour onto the objects that own the data, apply Tell, Don't Ask, and remove setters that allow invalid transitions.

Not every anemic class is a mistake. DTOs, read models and simple CRUD apps are meant to be data-only. It is a problem when the domain is complex and the rules have nowhere to live.

Practice. Pick an entity with only getters and setters, list every rule applied to it elsewhere, and move those rules into the entity.

DTOs Core

A data transfer object is a simple, behaviour-free structure used to move data across a boundary: an API request or response, a message on a queue, or a call between layers.

Why not send domain objects directly? Because the API contract and the domain model change for different reasons. Exposing entities leaks internal fields, ties clients to your database shape, and risks over-posting, where a client sets a field such as isAdmin that it should not. A DTO lets you shape exactly what goes over the wire and version it separately.

public record CreateOrderRequest(String customerId, List<LineDto> lines) {}
public record OrderResponse(String id, String status, BigDecimal total) {}

Mappers convert between database rows, domain objects and DTOs. Hand-written functions or compile-time generators such as MapStruct are easier to debug than reflection-based auto-mappers. Test them for nulls, dates and money.

Practice. Replace an endpoint that returns a database entity with a response DTO and remove every field clients do not need.

Repositories Core

A repository gives the domain a collection-like interface for loading and saving aggregates, hiding how and where they are stored.

interface OrderRepository {
  Optional<Order> findById(OrderId id);
  void save(Order order);
  List<Order> findUnpaidOlderThan(Duration age);
}

The interface belongs to the domain or application layer and speaks in domain terms; the implementation sits in infrastructure and uses SQL or an ORM. That makes business logic testable with an in-memory fake.

Common mistakes: a generic repository with getAll and update for every table, which adds nothing over the ORM, and leaking query objects or IQueryable types so callers depend on the database anyway.

Practice. Define a repository interface for one aggregate, implement it twice, once in memory and once with your database, and run the same tests against both.

ORMs Core

An object-relational mapper converts between objects and relational tables, generates SQL and tracks changes. Examples are Hibernate and JPA, Entity Framework Core, SQLAlchemy, Django ORM, ActiveRecord and Prisma.

They save a great deal of boilerplate, but they leak. Know these pitfalls:

  • N+1 queries: loading a list then lazily loading a relation per item. Fix with eager loading or joins.
  • Accidental full-table loads and unbounded queries.
  • Domain classes shaped by ORM annotations and requirements, such as public no-argument constructors.
  • Long-lived sessions holding stale data.

Read the SQL your ORM generates, and drop to hand-written SQL for reporting or performance-critical queries. The SQL Roadmap covers the database side.

Within a session, ORMs keep identity maps, so loading the same row twice returns the same object, and a unit of work writes all changes in one transaction. Keep sessions per request to avoid stale data.

Practice. Turn on SQL logging, find an N+1 query in a small app and fix it with eager loading.

Use Cases Core

A use case, also called an application service or interactor, represents one thing a user or system can do with the application: place order, approve refund. It coordinates the work but holds little business logic itself.

class ApproveRefund:
    def __init__(self, refunds, payments, clock):
        self.refunds, self.payments, self.clock = refunds, payments, clock

    def execute(self, refund_id, approver):
        refund = self.refunds.get(refund_id)
        refund.approve(approver, self.clock.now())   # rules live in the entity
        self.payments.issue(refund.amount, refund.customer)
        self.refunds.save(refund)

Use cases form the application's public API, independent of HTTP or CLI. They are also where transactions, authorisation checks and events are coordinated.

Practice. Extract the logic of one fat controller into a use case class and call it from both an HTTP route and a test.

Stage 7 Domain-Driven Design

Advanced2–3 weeks7 topics

Goal. Model a complex business domain with a shared language, clear boundaries, entities, value objects and aggregates.

Domain Driven Design Core

Domain Driven Design (DDD) is an approach for software with complex business rules. Its core claim is that the hardest part is understanding the domain, so developers and domain experts should build a shared model and the code should express it directly.

DDD has two halves:

  • Strategic design: splitting a large domain into bounded contexts, deciding how they relate, and investing most effort in the core domain that makes the business different.
  • Tactical design: building blocks inside a context, such as entities, value objects, aggregates, domain events, repositories and domain services.

DDD is overkill for CRUD apps. It pays off where rules are intricate, change often and carry business risk. Many teams use the strategic ideas even when they skip the tactical ones.

Model-Driven Design ties the two halves together: the code is the model, so new understanding changes the code, and awkward code points at a gap in understanding.

Practice. Pick a domain such as food delivery and list its subdomains, marking which one is core, supporting or generic.

Domain Language Core

The ubiquitous language is a shared vocabulary that developers, product people and domain experts all use, in conversation, documents, tests and code. If underwriters say a policy is lapsed, the code has policy.lapse(), not setStatus(3).

Building it is active work: run modelling sessions, write down terms and their exact meaning, and challenge words that mean different things to different people. Event storming, a workshop where people map domain events on sticky notes in time order, is a popular way to surface the language and the flow together.

When the same word means different things in different parts of the business, such as customer for sales and for support, that is a signal for separate bounded contexts, each with its own precise meaning.

Practice. Write a glossary of 15 terms for a domain you know, then rename classes and methods in a project to match it.

Boundaries Core

A bounded context is an explicit boundary inside which one model and one language apply consistently. Product in the catalogue context has descriptions and images; product in the warehouse context has weight and shelf location. Forcing one shared class to serve both creates a bloated, fragile model.

A context map describes how contexts relate:

  • Customer and supplier: one team's output feeds another's needs.
  • Conformist: the downstream simply accepts the upstream model.
  • Anti-corruption layer: the downstream translates the upstream model into its own, protecting itself.
  • Shared kernel: a small piece of model owned jointly.
  • Open host service with a published language: a well-documented API for many consumers.

Bounded contexts are the best starting point when splitting a system into modules or services.

Practice. Draw a context map for an online shop with at least four contexts and label each relationship.

Entities Core

An entity is a domain object defined by its identity, not its attributes. A customer who changes their name and address is still the same customer, because the ID stays the same. Entities have a lifecycle: created, changed over time and sometimes archived.

Design guidance:

  • Give each entity a stable ID, often generated by the application, such as a UUID, so it exists before saving.
  • Equality compares IDs, not fields.
  • Put behaviour on the entity and protect its invariants; avoid public setters.
  • Keep entities free of framework and persistence concerns where you can.

Not everything with an ID in the database needs to be an entity in the model; many things are better as value objects.

Practice. Model an Order entity with an ID, a status lifecycle and methods for each allowed transition, rejecting illegal ones.

Value Objects Core

A value object is defined only by its attributes and has no identity. Two Money(10, "GBP") objects are interchangeable. Value objects are immutable: to change one, you create a new one.

from dataclasses import dataclass

@dataclass(frozen=True)
class Money:
    amount: int        # pence, avoids float rounding
    currency: str

    def add(self, other: "Money") -> "Money":
        if other.currency != self.currency:
            raise ValueError("currency mismatch")
        return Money(self.amount + other.amount, self.currency)

They are the cure for primitive obsession: passing raw strings and numbers for emails, money, dates ranges and quantities. Validation happens once in the constructor, so every instance is valid. Use them generously.

Practice. Replace raw strings and numbers for email, money and quantity in one module with validated value objects.

Aggregates Core

An aggregate is a cluster of entities and value objects treated as one unit for changes, with a single entry point called the aggregate root. An Order root owns its OrderLines; outside code changes lines only through the order, so the order can enforce rules such as a maximum total.

Rules that interviewers probe:

  • One transaction changes one aggregate.
  • Other aggregates are referenced by ID, not by object reference.
  • Keep aggregates small; large ones cause locking conflicts and slow loads.
  • Consistency across aggregates is eventual, usually via domain events.

Repositories exist per aggregate root, not per table.

Practice. Design the aggregates for a ticket booking system and justify where each boundary sits based on which rules must be instantly consistent.

Domain Events Recommended

A domain event records something meaningful that has happened in the business, named in past tense: OrderPlaced, PaymentFailed. Aggregates raise events when their state changes, and other parts of the system react.

Uses:

  • Keep aggregates decoupled: placing an order does not need to call loyalty, email and analytics directly.
  • Achieve eventual consistency between aggregates and bounded contexts.
  • Feed audit logs and event sourcing.

Distinguish in-process domain events from integration events published to other services. To publish reliably, use the transactional outbox: write the event to a table in the same transaction as the state change, and have a separate process forward it to the message broker.

Practice. Add an OrderPlaced event to an order aggregate and two handlers that react to it without the order knowing about them.

Stage 8 Architectural Principles

Advanced2 weeks5 topics

Goal. Structure a codebase into components with stable boundaries, so business rules stay independent of frameworks and infrastructure.

Architectural Principles Core

Software architecture is the set of decisions that are expensive to change later: how the system is split into parts, how they communicate, where data lives and which qualities, such as scalability, availability or changeability, matter most.

Principles that guide good architecture:

  • Separate policy from detail and point dependencies toward policy.
  • Draw boundaries where things change for different reasons or at different rates.
  • Defer decisions about frameworks, databases and delivery until you have to make them.
  • Group classes using the component principles: things that change together ship together, and dependencies between components never form cycles.
  • Make every important decision explicit, with its trade-offs written down in an architecture decision record.
  • Choose based on quality attributes and constraints, not fashion.

Every architectural choice is a trade-off. In interviews, name what you gain, what you give up, and when you would choose differently.

Practice. Write an architecture decision record for a past technical decision, including options rejected and why.

Policy vs Detail Core

Policy is the business rules and decisions that give the system its value. Details are the mechanisms that let policy talk to the world: the database, the web framework, the message broker, the UI. Policy vs Detail is the distinction behind most modern architecture advice.

Policy should not depend on details. If tax rules import an ORM class, a database change can break tax rules. Instead, policy defines interfaces for what it needs, and details implement them. This is dependency inversion at architecture scale.

A practical consequence: you should be able to run your core business logic in a unit test with no database, web server or network. If you cannot, details have leaked into policy.

Keep framework code distant for the same reason: annotations and framework base classes belong in controllers and adapters, never in entities or use cases.

Practice. Run the core logic of one use case in a test with no I/O at all, adding interfaces where details get in the way.

Layered Architectures Core

A layered architecture splits code into horizontal layers, each depending only on the one below. The classic stack is presentation, application, domain and infrastructure or data access.

Newer variants turn the dependencies inward:

  • Hexagonal, also called ports and adapters: the core defines ports, which are interfaces, and adapters implement them for HTTP, databases or queues.
  • Onion and clean architecture: concentric circles with entities in the middle, use cases around them and frameworks outside. Source code dependencies point only inward.

The classic layered style is simple and familiar but often lets the domain depend on the database. The inward variants protect the core at the cost of more interfaces and mapping. Pick the lightest one your complexity justifies.

Practice. Draw the layers of a project you know and mark every dependency that points the wrong way.

Organize code by actor it belongs to Recommended

Group code by the people or business capability it serves, not by technical type. A folder structure of controllers, services and repositories scatters each feature across the tree. A structure of billing, shipping and catalogue, each holding its own controller, logic and data access, keeps a feature in one place.

src/
  billing/     invoice.py  billing_service.py  billing_api.py
  shipping/    shipment.py rates.py            shipping_api.py
  shared/      money.py

This links to single responsibility: an actor is a group of users whose requests drive change. It also makes the architecture show what the system does at a glance, and it prepares clean seams if you later extract services. Vertical slice architecture pushes the same idea down to individual features.

Practice. Reorganise a small project from technical folders into feature folders and compare how many folders a typical change touches.

Architectural Styles and Patterns Core

The terms overlap and people use them loosely, but a helpful split is:

  • Architectural Styles describe the overall shape and constraints of a system: monolithic, layered, client-server, event-driven, microservices, serverless.
  • Architectural Patterns are reusable solutions to a recurring problem within some style: CQRS, event sourcing, API gateway, backend for front end, saga, circuit breaker, strangler fig.

Styles are often grouped by how they are deployed and how parts communicate:

  • Structural styles: layered, component based, microkernel.
  • Messaging styles: event driven, publish subscribe.
  • Distributed styles: client server, peer to peer, SOA, microservices.

Real systems combine several. Being able to place a system in this vocabulary is a frequent senior interview task.

Practice. Describe an application you know using at least one style and two patterns, and say why each was chosen.

Stage 9 Architectural Styles

Advanced2–3 weeks8 topics

Goal. Compare the main architectural styles and choose one for a given product, team and set of quality requirements.

Monolithic Core

A monolithic architecture deploys the whole application as one unit, usually with one database. It is the right default for most new products: simple to develop, test, deploy and debug, with in-process calls and ordinary transactions.

Its problems show up with scale of team rather than scale of traffic: slow builds, merge conflicts, risky deployments and tangled code where everything can call everything. Most of those come from poor internal boundaries, not from being a monolith.

A modular monolith keeps one deployable but enforces strict module boundaries aligned with bounded contexts, with each module owning its tables and exposing a small API. It gives most of the design benefits of services without the distributed-systems cost, and makes later extraction straightforward.

Practice. Split a small monolith into three modules with enforced boundaries and no shared tables between them.

Client Server and Peer to Peer Core

In client-server architecture, clients send requests and servers respond with data or services. The web, mobile apps calling APIs, and database connections all follow it. The server centralises data, security and business rules; clients focus on presentation and user interaction.

Variants are described by tiers: two-tier puts the client directly on the database, three-tier inserts an application server between them, and n-tier adds more, such as caches and gateways.

Design questions include where validation runs (always on the server, optionally also on the client), how much logic sits in thick versus thin clients, and how to handle many clients on different versions. Servers should be stateless where possible so you can add more behind a load balancer.

Peer to peer removes the central server: every node is both client and server, as in BitTorrent. There is no single point of failure, but discovery and trust are hard, so many products go hybrid, as WebRTC calls do.

Practice. Draw the tiers of a mobile app you use and note where data validation and authorisation must happen.

Microservices Core

Microservices split a system into small services, each owning one business capability and its own data, deployed independently and communicating over the network.

What you gain: independent deployment and scaling, team autonomy and fault isolation. What you pay: network latency and failures, no cross-service transactions, eventual consistency, distributed tracing and much more operational work.

Key practices: draw boundaries along bounded contexts, give each service its own database, prefer asynchronous events for cross-service updates, use sagas instead of distributed transactions, and add timeouts, retries and circuit breakers to every call. The strangler fig pattern migrates a monolith gradually. Avoid the distributed monolith, where services share a database or must deploy together. Start with a modular monolith unless you have many teams.

Its predecessor, SOA, shared the idea of explicit service contracts but routed traffic through a central enterprise service bus that collected business logic. You still meet it in large enterprises.

Practice. Take a modular monolith design and list exactly what would change if you extracted one module into a service.

Event Driven Core

In an event-driven architecture, components communicate by producing and reacting to events rather than calling each other directly. The producer announces that something happened and does not know who reacts.

Event types to distinguish:

  • Event notification: a thin event; consumers call back for details.
  • Event-carried state transfer: the event includes the data, so consumers need not call back.
  • Domain events inside a service and integration events between services.

Benefits are loose coupling, easy extension with new consumers and natural buffering under load. Costs are harder debugging, eventual consistency, duplicate and out-of-order delivery, and schema evolution. Consumers must be idempotent, and every event needs a correlation ID for tracing.

Practice. Design the events for an order flow and write down what each consumer does if it receives the same event twice.

Messaging Core

Messaging is the infrastructure that carries events and commands between components asynchronously. Two main models:

  • Message queues such as RabbitMQ, Amazon SQS and Azure Service Bus: each message is delivered to one consumer in a group and removed once acknowledged. Good for distributing work.
  • Publish Subscribe: each message goes to every subscriber, so several systems can react to the same event independently.

Most brokers offer at-least-once delivery, so plan for duplicates. Use dead-letter queues for messages that keep failing, set retry limits with backoff, and decide whether ordering matters; ordering is usually only guaranteed per partition or queue. Separate commands, queries and events: a command goes to one owner, an event to anyone interested.

Message queues, streams and logs differ too. A queue deletes a message once acknowledged and consumers compete for it; a stream such as Kafka keeps an ordered log, each consumer group tracks its own offset, and you can replay history.

Practice. Set up a local broker, publish an event to two subscribers and a work queue to two competing consumers, and observe the difference.

CQRS Recommended

Command Query Responsibility Segregation uses separate models for writing and for reading. The write side, handling commands, enforces business rules on a normalised domain model. The read side serves queries from views shaped exactly for each screen, often denormalised and sometimes in a different store, such as a search index.

The read models are updated from the write side, often through events, so they are eventually consistent. Benefits: each side is simpler and can scale and be optimised separately. Costs: more moving parts, synchronisation code and users possibly seeing stale data briefly.

Use CQRS where reads and writes have very different shapes or loads. It does not require event sourcing, separate databases or microservices; the lightest form is just separate query classes that bypass the domain model.

Event sourcing is often paired with it: you store the events that changed an entity instead of its current state and rebuild state by replaying them. You gain a full audit trail and can build new projections from history, but pay with event versioning, projections and snapshots. Use it where history is the business, such as ledgers.

Practice. Add a dedicated read model for one slow dashboard query, updated from domain events, and compare the code with the original.

Serverless Architecture Recommended

Serverless Architecture builds applications from managed services and functions that run on demand, such as AWS Lambda, Azure Functions and Google Cloud Run functions, combined with managed queues, storage and databases. You do not manage servers, you pay per use and scaling is automatic.

Design implications:

  • Functions are stateless and short-lived, so state goes into managed stores.
  • Cold starts add latency to the first request after idle time.
  • Execution time, memory and concurrency are limited.
  • Architecture becomes event-driven: functions triggered by HTTP, queues, files and schedules.
  • Local testing and tracing across many functions are harder, and the design ties you to a vendor's services.

It suits spiky or low traffic, event processing and glue code. Steady heavy workloads are often cheaper on containers.

Practice. Build a function triggered by a file upload that creates a thumbnail, and measure its cold and warm start times.

Model View Controller Core

Model View Controller separates an interactive application into three roles. The model owns state and rules, the view renders that state, and the controller turns user input into calls on the model and picks what to show.

Server-side web frameworks such as Rails, Django, Laravel, ASP.NET Core and Spring MVC are built around it, although Django calls its variant model-template-view. Related patterns: MVP makes the view passive behind a presenter, and MVVM binds the view to a view model, as in many mobile and desktop frameworks.

The common failure is the fat controller holding business rules, or the fat model mixing persistence with every rule. Keep controllers thin, delegating to use cases or services.

Practice. Find the largest controller in a web project and move its business logic into a separate class that has no web types.

Projects to build

Build these in order. Each one should end up in a public repository with a short README that explains your decisions.

  1. Refactoring kataBeginner

    Take a known messy kata, such as a theatrical billing or inventory update exercise, write characterisation tests around it, then refactor it with named steps until each function is small and well named. Commit after every step.

    Refactoring · Meaningful names · Unit tests · Small functions

  2. Pluggable notification libraryIntermediate

    Build a library that sends notifications through email, SMS and chat. Use strategy for channels, decorator for retries and rate limiting, and a factory driven by configuration. Add a new channel without editing existing code.

    Interfaces · Strategy · Decorator · Open-closed principle

  3. Library loans service with clean architectureIntermediate

    Build a small back end for library loans with entities, value objects, use cases, repository interfaces and two adapters: in-memory for tests and a real database. Expose it over HTTP and a CLI using the same use cases, and add an architecture test that keeps the domain free of framework imports.

    Use cases · Repositories · Dependency inversion · Hexagonal architecture

  4. DDD model for event ticketingAdvanced

    Run a solo event-storming session for a ticketing business, define bounded contexts and a context map, and implement the booking context with aggregates and domain events. Publish events through an outbox to a second context that sends confirmations.

    Bounded contexts · Aggregates · Domain events · Transactional outbox

  5. Modular monolith to event-driven servicesAdvanced

    Build a shop as a modular monolith with enforced module boundaries, then extract one module into a separate service communicating through a message broker. Add a CQRS read model for order history and write architecture decision records for each step.

    Modular monolith · Messaging · CQRS · Architecture decision records

Interview questions

Try to answer each question out loud before you open the answer.

What is the difference between coupling and cohesion?Junior

Coupling is the degree to which a module relies on other modules; cohesion is how well the contents of one module belong together. We want low coupling, so changes stay local, and high cohesion, so each module has one clear purpose. A sign of trouble is a single feature change touching many unrelated files. I reduce coupling with narrow interfaces and dependency inversion, and raise cohesion by grouping code that changes for the same reason.

Explain the SOLID principles with an example of each.Mid

Single responsibility: a payroll class should not also format reports. Open-closed: add a new discount type as a new strategy class, not an edit to a switch. Liskov: a read-only list that throws on add breaks callers expecting a list. Interface segregation: split a large repository interface into reader and writer roles. Dependency inversion: the order service depends on an OrderStore interface it owns, implemented by a SQL adapter. They are guidelines, and I apply them where change actually happens.

What is an anemic domain model and why is it a problem?Mid

It is a model whose classes have only data and getters and setters, with all rules in service classes. Nothing protects invariants, so any service can put an object into an invalid state, and rules get duplicated. I fix it by moving behaviour onto the entity, removing setters that allow illegal transitions, and applying Tell, Don't Ask. For simple CRUD or DTOs, data-only classes are fine; it is a problem when the domain is complex.

Why is the singleton pattern controversial?Mid

A singleton is global mutable state with a nice name. Code reaches it through a static accessor, so dependencies are hidden, tests cannot easily replace it and state leaks between tests. It also complicates concurrency. When I need exactly one instance, I let the dependency injection container manage its lifetime and pass it through constructors, so the single-instance decision lives in one place and tests can supply a fake.

What is the difference between an entity and a value object?Mid

An entity has identity that persists through change: a customer is the same customer after changing address, so equality compares IDs. A value object is defined only by its attributes, has no identity and is immutable: two Money objects of ten pounds are equal and interchangeable. I prefer value objects wherever identity does not matter, because they validate once, are thread-safe and remove primitive obsession.

What is an aggregate, and how do you decide its boundary?Senior

An aggregate is a cluster of objects changed as one unit through a root that enforces its invariants. I draw the boundary around rules that must be consistent immediately, within one transaction, and keep everything else eventually consistent through domain events. Aggregates reference each other by ID. I keep them small, because large aggregates cause lock contention and slow loads, and I check the boundary by asking which rule would break if two users changed it concurrently.

What is a bounded context and how does it relate to microservices?Senior

A bounded context is a boundary within which one model and language are consistent; the same word, such as product, can mean different things in different contexts. Bounded contexts are the best guide to service boundaries because they align with how the business and teams split work. The mapping is not always one to one: a context can be one module in a monolith or a few services. I would draw contexts first, then decide deployment.

Compare a monolith, a modular monolith and microservices.Mid

A monolith is one deployable, simple to build and run but prone to tangled code. A modular monolith keeps one deployable but enforces module boundaries and data ownership, giving most design benefits cheaply. Microservices deploy independently and scale separately, at the cost of network failures, eventual consistency and heavy operations. For a new product or a small team I start with a modular monolith and extract services only when team size, scaling or release independence demands it.

Explain the difference between CQS and CQRS.Mid

CQS is a method-level rule: a method either changes state or returns data, not both. CQRS applies the split at architecture level: separate models, and possibly stores, for writes and reads. The write model enforces rules; read models are shaped for queries and updated from the write side, often via events, so they may be slightly stale. CQRS does not require event sourcing or separate databases; it can be as simple as dedicated query classes.

What is event sourcing, and when would you avoid it?Senior

Event sourcing stores every change as an event and rebuilds current state by replaying them. It gives a perfect audit trail and lets you build new projections from history. I avoid it for typical CRUD systems because it brings event versioning, projections, snapshots and eventual consistency, and it is hard to undo once adopted. I would use it where history is the core of the business, such as ledgers, and keep it within one bounded context.

What is dependency inversion and how does hexagonal architecture use it?Mid

Dependency inversion says high-level policy should not depend on low-level details; both depend on abstractions owned by the policy. In hexagonal architecture the core defines ports, such as a PaymentPort or OrderRepository, and adapters for HTTP, databases or vendors implement or call them. Source dependencies point inward, so the core can be tested without infrastructure and adapters can be replaced without touching business rules.

How do you guarantee an event is published when a database transaction commits?Senior

Writing to the database and publishing to a broker are two systems, so one can succeed while the other fails. I use the transactional outbox: in the same transaction as the state change, insert the event into an outbox table. A relay process, or change data capture, reads the outbox and publishes to the broker, marking rows as sent. Delivery becomes at least once, so consumers must be idempotent, for example by recording processed event IDs.

How would you start improving a large legacy codebase with no tests?Senior

I would not rewrite it. First I find the areas that change most and hurt most, using version history and bug data. Around those, I add characterisation tests that pin current behaviour, then refactor in small, safe steps, extracting seams where I can inject dependencies. For larger structural change I use the strangler fig pattern, routing features one at a time to new code. I record decisions and agree module boundaries with the team so improvements stick.

Common mistakes

  • Learning patterns as recipes and forcing them into code; start from the problem and add a pattern only when it removes real pain.
  • Treating DRY as removing any similar-looking code; merge only duplicated knowledge that changes for the same reason.
  • Adding an interface for every class by reflex; introduce abstractions at boundaries or where there are real alternatives.
  • Building anemic entities with public setters and putting all rules in services; move behaviour onto the objects that own the data.
  • Exposing database entities directly through APIs; use DTOs so the contract and the model can change independently.
  • Starting a new product with microservices; begin with a well-structured modular monolith and extract services when there is a clear reason.
  • Using deep inheritance hierarchies for code reuse; prefer composition and keep inheritance shallow.
  • Mixing refactoring and behaviour changes in one commit; separate them so each can be reviewed and reverted safely.
  • Describing an architecture without its trade-offs; always say what you give up and when you would choose differently.

Ready-for-interview checklist

  • I can explain coupling and cohesion and point to examples of each in real code.
  • I can explain each SOLID principle with an example and a case where applying it would be overkill.
  • I can refactor a long function into small, well-named steps with tests passing throughout.
  • I can implement strategy, observer, decorator, adapter, factory and builder from memory.
  • I can explain why singleton is often considered an anti-pattern and what to use instead.
  • I can compare transaction scripts with a domain model and say when each fits.
  • I can design a repository interface and explain where it lives and why.
  • I can model a domain with entities, value objects and aggregates and justify aggregate boundaries.
  • I can draw a context map and explain an anti-corruption layer.
  • I can explain layered, hexagonal and clean architecture and the direction of dependencies in each.
  • I can compare monolith, modular monolith, microservices and serverless for a given scenario.
  • I can explain event-driven architecture, queues versus streams, CQRS and event sourcing with their costs.
  • I can describe the transactional outbox and why consumers must be idempotent.

Related roadmaps

FAQ

What is the difference between software design and software architecture?

Design usually refers to decisions inside a component: classes, functions, names and patterns. Architecture covers the decisions that are expensive to reverse: how the system splits into parts, how they communicate and which quality attributes matter. The line is blurry, and the same principles, such as coupling, cohesion and dependency direction, apply at both scales.

Do I need to learn all 23 design patterns?

No. Learn about ten well: strategy, observer, decorator, adapter, facade, factory, builder, singleton, command and template method. Know the rest well enough to recognise their names and the problem each solves. Understanding why patterns work, through principles such as composition and encapsulating what varies, matters more than memorising the catalogue.

How long does it take to learn software design and architecture?

The core of clean code, SOLID and common patterns takes two to three months of steady practice alongside real coding. Domain modelling and architectural judgement take longer because they grow from experience with systems that change over time. Following this roadmap at about eight hours a week, plan for four to five months to be interview-ready for mid-level roles.

Should I learn system design or software architecture first?

Learn the design principles here first, at least up to design patterns, because they shape how you structure any code. Then study the System Design Roadmap, which focuses on scaling distributed systems: load balancing, caching, sharding and consistency. The architecture stages of this roadmap sit between the two and help in both kinds of interview.

Practise while you learn.Solve interview problems in the browser, with hints and a timer.
CodingSQLSystem design
Next Post Previous Post
No Comment
Add Comment
comment url