skip to content

Architectural Principles

The same design instincts applied at system scale: where boundaries go, which way dependencies point, how to layer, and what belongs in one component. These are the principles you cite when justifying a module structure to a room of architects.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

What is an architectural boundary in a software system, and what is the difference between a module's interface and its implementation?

level: juniorimportance: must knowfreq 72%

answer

  1. interface = what, implementation = how
  2. hide the decision most likely to change (Parnas)
  3. high cohesion inside, low coupling across
  4. arrows point one way; invert to keep them so
  5. unenforced boundary = a drawing

basics

~20 s

A boundary is a line separating parts of a system that change for different reasons. The interface is the small set of operations other parts may call; the implementation is the hidden internals (data structures, algorithms, storage) they must not touch.

solid answer

~50 s

An architectural boundary is a deliberate separation line between two parts of a system, across which dependencies are restricted and explicit. Each side exposes an interface — a named contract of operations, inputs and outputs — while keeping its implementation private: internal data layout, algorithms, database tables, third-party libraries, error details. The point is substitutability and independent change: if callers only depend on the contract, the owner can rewrite internals, swap storage, or re-deploy without breaking anyone. A boundary is real only if it is enforced; if callers can reach around the interface (import an internal class, query the other module's tables, deserialize its private DTO), the boundary is decorative. Good boundaries follow high cohesion inside and low coupling across, and are placed where change axes differ. Boundaries also cost something to cross — serialization, indirection, latency, extra tests — so you place them where the decoupling pays for that cost, not everywhere.

code

pseudocode · 12 lines
pseudocode
// Leaky: caller now depends on the storage schema and the vendor SDK
interface Payments {
  PaymentRow findRow(long id);          // DB row escapes
  void charge(StripeChargeRequest req); // vendor type escapes
}

// Sealed: contract types owned by the module, intent-shaped
interface Payments {
  ChargeResult charge(OrderId order, Money amount);  // ChargeResult/Money are ours
  PaymentStatus statusOf(OrderId order);             // no rows, no vendor errors
}
// Internals (tables, SDK, retry policy) are free to change.

go deeper

for a junior

Define boundary, interface, implementation; give one concrete leak (returning a DB entity) and why it hurts.

for a middle

Add information hiding as 'hide the decision likely to change', cohesion/coupling placement, and one-way dependency direction.

for a senior

Discuss dependency inversion to preserve direction, contract ownership and translation at the edge, and the cost side — why not every seam deserves a boundary.

for a principal

Frame boundaries as options on future change: what independent-change right are we buying, at what coordination and latency cost, and what mechanism makes it real rather than aspirational.

## What a boundary is An **architectural boundary** is a line you draw through a system that says: *code on this side may only talk to code on that side through a declared contract, and only in a declared direction.* Boundaries exist at many scales — a function, a class, a package/module, a library, a process, a service, a whole system — and the same idea applies at each. Two things define a boundary: 1. **A contract (interface)** — the public surface: operation names, parameters, return values, errors, and the semantics/guarantees attached to them (ordering, idempotency, units, validity rules). 2. **A rule about dependencies** — who is allowed to know about whom. Crossing outside the contract is forbidden, and the *direction* of knowledge is controlled. ## Interface vs. implementation - **Interface** = *what* it does, from the caller's point of view. `chargeCard(orderId, amountMinorUnits, currency) -> ChargeResult`. Stable, small, expressed in the caller's vocabulary. - **Implementation** = *how* it does it. Which payment gateway, which retry policy, which table schema, which caching library, whether it is one class or forty. **Information hiding** (David Parnas, 1972) is the underlying principle: modules should be decomposed so that each hides a *design decision likely to change*, and the interface reveals as little of that decision as possible. Parnas's key insight is that you don't decompose by processing steps ("step 1 module, step 2 module") but by *secrets*. What counts as a secret you should hide: - storage technology and schema (tables, indexes, column names) - third-party SDK types and vendor error codes - internal data structures and invariants - concurrency/threading choices - serialization format used internally - whether the work is done in-process, in a queue, or by a remote call ### Leaky interfaces An interface **leaks** when it forces callers to know a hidden decision. Classic leaks: - Returning your ORM entity / database row object. Now every caller depends on your schema, and a column rename is a breaking change. - Throwing vendor-specific exceptions (`SQLException`, `StripeCardError`) across the boundary. - Exposing a `Map<String, Object>` "bag" whose keys are really your internal field names. - Exposing mutable internal collections, so callers can corrupt your state. - CRUD-shaped interfaces (`getRow`, `setField`) instead of intent-shaped ones (`cancelSubscription`). The fix is usually a **translation step at the edge**: the module converts internal representations into a contract type it owns, and converts vendor errors into its own error vocabulary. ## Why boundaries are worth the trouble - **Independent change / replaceability.** You can rewrite the inside without a system-wide ripple. The unit of *replacement* is the unit bounded by an interface. - **Independent reasoning.** A reader can understand one side without loading the other into their head. This is often the biggest practical win. - **Independent testing.** You can substitute a fake implementation of the contract, so tests are fast and deterministic. - **Independent deployment (sometimes).** If a boundary is also a process boundary, the two sides can ship on separate schedules. - **Blast-radius control.** A bug, a bad dependency, or a security-sensitive concern stays on one side. ## Cohesion and coupling The usual heuristic: **high cohesion inside, low coupling across.** Things that change together should live on the same side of the line; things that change for different reasons should be separated. A useful test: look at your last 20 commits or pull requests. If most of them touch both sides of a proposed boundary, the boundary is in the wrong place — you have split something that is really one thing, and now you pay coordination cost for nothing. ## Directionality: dependencies point one way A boundary is much stronger when the dependency arrow only goes one way. If `Orders` calls `Payments` *and* `Payments` calls `Orders`, neither can be understood, tested, or deployed alone — you have a cycle, which is effectively one big module with extra ceremony. When you need a call in the "wrong" direction, **dependency inversion** lets you keep the arrow: the low-level side defines and depends on an interface *owned by* the high-level side (or by a shared contract module), and the concrete implementation is plugged in at composition time. The source-code dependency now points opposite to the runtime call. Callbacks, ports-and-adapters, and event publication are all forms of this. ## Boundaries are not free Every boundary costs: an extra type to map to, an extra indirection to read through, extra tests, sometimes serialization and network latency. Premature boundaries produce "architecture astronaut" code — five interfaces with one implementation each and no independent change ever realized. Draw boundaries where you have evidence of a real change axis (different rate of change, different team, different scaling profile, different failure/security domain, a genuinely replaceable vendor). ## A boundary that isn't enforced isn't a boundary If anything can `import` anything, the line exists only in a diagram. Real enforcement comes from language/module visibility (private, internal, package-private, module exports), build-level module separation, dependency-rule tests (fitness functions), review, and — the strongest and most expensive — physical separation into separate processes with no shared database.

  • If a module returns its own contract type instead of a database row, haven't you just duplicated the fields?
    Often yes, and that duplication is the point: it buys the freedom to evolve the two independently. The internal shape can grow columns, denormalize, or split tables while the contract type stays stable. You only merge them when the module is genuinely trivial and the boundary earns nothing.
  • How can you tell a boundary is in the wrong place?
    Change coupling: most changes touch both sides at once, contracts churn on nearly every feature, or every read requires a chatty back-and-forth across the line. Those are signs one concept was split; move the line or remove it.

A restaurant menu is the interface; the kitchen is the implementation. You order 'soup of the day' and get a bowl. The chef can change supplier, recipe, or the whole stove layout without reprinting your relationship with the restaurant. The moment diners are allowed to wander into the kitchen and grab pans, the menu stops being a boundary.

context

open as a page

What are the three component cohesion principles (REP, CCP, CRP) described by Robert C. Martin, and what does each one say about which code belongs together in a releasable component?

level: juniorimportance: must knowfreq 72%

basics

~20 s

REP: things released together should be usable together. CCP: put classes that change for the same reason in the same component. CRP: don't force users to depend on code they never use. Together they decide component contents.

open as a page

What is the Dependency Inversion Principle (DIP), and how does introducing an interface change which way a dependency points between a high-level policy and a low-level detail?

level: juniorimportance: must knowfreq 82%

basics

~20 s

DIP says high-level policy should not depend on low-level details; both should depend on an abstraction. You define an interface next to the policy, the policy calls it, and the detail implements it — so the arrow now points from the detail to the policy.

open as a page

In software design, what do the terms "coupling" and "cohesion" mean, and why is the standard goal low coupling with high cohesion?

level: juniorimportance: must knowfreq 82%

basics

~20 s

Coupling is how much one module depends on another; cohesion is how well the things inside one module belong together. Low coupling means changing one module rarely forces changes elsewhere; high cohesion means each module does one clear job.

open as a page

In a layered software architecture with presentation, domain (business logic), and infrastructure/persistence layers, what is each layer responsible for and in which direction may dependencies point?

level: juniorimportance: must knowfreq 85%

basics

~20 s

Presentation handles input and output (UI, HTTP endpoints). Domain holds business rules and decisions. Infrastructure talks to databases, files, and other systems. Dependencies point one way — upper layers know about lower ones, never the reverse.

open as a page

What is Separation of Concerns in software architecture, and how would you tell whether a system actually follows it?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Separation of Concerns means each part of a system deals with one distinct topic — for example, storing data, applying business rules, or drawing the screen — so you can understand or change one part without touching the others.

open as a page

What does 'boundary-crossing cost' mean, and how should the cost of a particular boundary change the interface you design across it?

level: middleimportance: must knowfreq 58%

basics

~20 s

Crossing a boundary costs something: a function call is nearly free, a call to another process costs serialization, network latency and possible failure. Cheap boundaries can have fine-grained, chatty interfaces; expensive ones need coarse-grained calls that return everything needed at once.

open as a page

The Common Closure Principle (CCP) says to gather into one component the classes that change for the same reasons at the same times. What concrete cost does it reduce, how do you decide the grouping in practice, and how does it relate to the Single Responsibility and Open/Closed principles?

level: middleimportance: must knowfreq 58%

basics

~20 s

CCP keeps code that changes together in one releasable unit, so a typical requirement change touches one component instead of many — one thing to build, test, version and deploy. It is the Single Responsibility Principle applied to components.

open as a page

What is the Common Reuse Principle (CRP), what real cost does an unused dependency inside a component impose on its consumers, and how is CRP related to the Interface Segregation Principle?

level: middleimportance: must knowfreq 52%

basics

~20 s

CRP says don't force users of a component to depend on things they don't need. Classes used together belong together; classes used separately belong apart. It is the Interface Segregation Principle applied to whole components.

open as a page

Clean/Onion/Hexagonal architectures all state a Dependency Rule: source dependencies may only point one way. Which way, why that way, and how does it turn infrastructure into a plugin?

level: middleimportance: must knowfreq 68%

basics

~20 s

Source dependencies point inward, toward policy: UI and database depend on use cases, use cases depend on the domain, and nothing inner knows anything outer. Because the inner circles define the interfaces, outer pieces plug into them and can be swapped like plugins.

open as a page

What is the Dependency Inversion Principle (DIP), and how does a code-level rule about interfaces end up creating an architectural boundary?

level: middleimportance: must knowfreq 74%

basics

~20 s

DIP says high-level policy should not depend on low-level details — both should depend on an abstraction, and that abstraction should be owned by the policy side. In practice the business code declares the interface and the database or HTTP code implements it, so the dependency arrow points inward.

open as a page

What is the difference between strict (closed) layering and relaxed (open) layering, and what are the trade-offs of each?

level: middleimportance: must knowfreq 62%

basics

~20 s

Strict layering lets a layer call only the layer immediately below it. Relaxed layering lets it call any lower layer, skipping levels. Strict gives better isolation and swappability; relaxed avoids pointless pass-through code but couples more layers together.

open as a page

What are cross-cutting concerns, why do they resist ordinary modularization, and what mechanisms exist to handle them?

level: middleimportance: must knowfreq 68%

basics

~20 s

Cross-cutting concerns are needs like logging, security, or transactions that every part of the system uses, so they can't sit in one module. They're handled by pulling them into shared wrappers — middleware, interceptors, decorators — instead of copying the code everywhere.

open as a page

How do logical boundaries (modules, bounded contexts) relate to physical deployable units (processes, services)? Does every bounded context need its own service?

level: seniorimportance: must knowfreq 56%

basics

~20 s

No. A logical boundary says who may depend on whom; a deployable unit says what ships and runs separately. You can have strong logical boundaries inside one deployable process (a modular monolith) and only split into services when you need independent deployment, scaling, or isolation.

open as a page

Explain the component cohesion "tension triangle" formed by REP, CCP and CRP: what does a project suffer if it sits at each edge, and how should its position move over the system's lifetime?

level: seniorimportance: must knowfreq 45%

basics

~20 s

REP and CCP make components bigger; CRP makes them smaller, so you cannot satisfy all three. Ignore CRP and consumers get needless releases; ignore CCP and one change touches many components; ignore REP and the components are unusable by others.

open as a page

What do the Liskov Substitution Principle and the Interface Segregation Principle actually require, and how do violations of each show up above the code level — in service contracts and public APIs?

level: seniorimportance: must knowfreq 58%

basics

~20 s

LSP: any implementation of a type must be usable wherever that type is expected, without callers needing to know which one they got. ISP: don't force a client to depend on methods it doesn't use — prefer several small, role-specific interfaces over one fat one.

open as a page

How can a domain (business) layer trigger database writes, emails, or messages without having any compile-time dependency on the database, mail, or broker technology?

level: seniorimportance: must knowfreq 58%

basics

~20 s

The domain declares interfaces describing what it needs ("save this order", "send this notification") in its own vocabulary. Infrastructure classes implement those interfaces, and something at startup wires the implementations in. Calls still go outward; the code dependency points inward.

open as a page

Compare horizontal layering with vertical feature slicing as ways of partitioning a system. What does each optimize for, and how do you choose?

level: seniorimportance: must knowfreq 61%

basics

~20 s

Layering groups code by technical role — UI, business logic, data access — so each technology is isolated. Vertical slicing groups code by feature, so everything for "checkout" sits together. Layers optimize for swapping technology; slices optimize for changing one feature.

open as a page

What is an anti-corruption layer, and when would you place one at a boundary between two systems?

level: middleimportance: should knowfreq 44%

basics

~20 s

An anti-corruption layer is translation code at a boundary that converts another system's model, names and errors into your own. It stops a legacy or third-party design from leaking into and distorting your code, and localizes the damage when that system changes.

open as a page

What is the Acyclic Dependencies Principle, what concrete problems do dependency cycles between components cause, and what are the two standard ways to break a cycle?

level: middleimportance: should knowfreq 52%

basics

~20 s

The Acyclic Dependencies Principle says the component dependency graph must have no cycles. Cycles make components impossible to build, test, or release separately. You break them either by inverting one edge with an interface, or by extracting the shared part into a new component both depend on.

open as a page

David Parnas's 1972 paper "On the Criteria To Be Used in Decomposing Systems into Modules" argues for information hiding. What criterion does it propose for drawing module boundaries, and how does that differ from encapsulation as a language feature?

level: middleimportance: should knowfreq 42%

basics

~20 s

Parnas says each module should hide one design decision that is likely to change — not represent one step of the processing flow. Encapsulation is the language mechanism (private fields, accessors) you might use; information hiding is the design decision about what to conceal and why.

open as a page

How do you decide where to draw boundaries when designing a new system or decomposing an existing one?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Put boundaries where things change for different reasons and rarely change together. Group by business capability and by who owns the data, not by technical layer; check candidate lines against real change history and against how chatty the two sides would be.

open as a page

The Reuse/Release Equivalence Principle (REP) states that "the granule of reuse is the granule of release". What does that demand in practice, and what concretely goes wrong when a team publishes a component without release discipline?

level: seniorimportance: should knowfreq 34%

basics

~20 s

REP means anything you want reused must be released as a versioned, documented unit with release notes, so consumers can tell what changed and choose when to upgrade. Its contents must form one coherent, explainable theme.

open as a page

Distinguish source-code, build/binary, and runtime dependency direction. When a service calls another service over HTTP, in what sense can that dependency still be 'inverted', and who should own the contract?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Source dependency = whose code names whose; build dependency = which artifact must be present to compile/link; runtime dependency = who calls whom and must be running. Over HTTP the caller still needs the callee at runtime, but you can invert the source/contract direction by having the consumer define the interface it needs and the provider satisfy it.

open as a page

Explain the Stable Dependencies Principle and the Stable Abstractions Principle, including the instability (I) and abstractness (A) metrics, the Main Sequence, and the Zone of Pain and Zone of Uselessness.

level: seniorimportance: should knowfreq 34%

basics

~20 s

SDP: depend in the direction of stability — a component should only depend on components harder to change than itself. SAP: the more stable a component, the more abstract it should be. Instability I = outgoing/(incoming+outgoing) dependencies; abstractness A = abstract types / all types. Good components sit near the line A + I = 1.

open as a page

Structured design ranks coupling from content coupling down to data coupling, and cohesion from coincidental up to functional. Walk through those two scales and explain how you would actually use them when reviewing a module boundary.

level: seniorimportance: should knowfreq 38%

basics

~20 s

Coupling, worst to best: content (reaching into another module's internals), common (shared global data), external, control (passing a flag that steers the callee's logic), stamp (passing a whole record when only a field is needed), data (passing just the needed values). Cohesion, worst to best: coincidental, logical, temporal, procedural, communicational, sequential, functional (one job).

open as a page

Name the common ways teams break a layered architecture in practice (layer-bridging and leakage anti-patterns) and explain why each is harmful.

level: seniorimportance: should knowfreq 48%

basics

~20 s

Typical breakages: controllers containing business rules, database entities used directly as API responses, lower layers calling upward, skipping the domain to hit the database, and pass-through layers that add nothing. Each one couples layers that should change independently.

open as a page

What does it mean to decompose a system by isolating volatility (rather than by functional decomposition), and how do you identify the volatile areas?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Instead of creating a module per business function, you create modules around the things most likely to change — a payment provider, a tax rule, a storage technology — and hide each behind a stable interface, so future changes stay inside one module.

open as a page

How do you stop architectural boundaries from eroding over time? What mechanisms actually enforce them, and how strong is each?

level: principalimportance: should knowfreq 36%

basics

~20 s

Documents and good intentions don't hold. Enforce boundaries mechanically: language/module visibility so internals aren't importable, automated dependency-rule tests in CI that fail the build on violations, separate build modules, and — strongest and most costly — separate processes with separate databases.

open as a page

How do the component cohesion principles (REP, CCP, CRP) apply when the "components" are independently deployed services or packages in a large organisation, and what evidence would you use to justify merging or splitting one?

level: principalimportance: should knowfreq 28%

basics

~20 s

A service is a releasable component, so the same rules hold: keep code that changes together in one service (CCP), avoid shared libraries that force everyone to redeploy (CRP), and version every published contract (REP). Justify changes with co-change and release data.

open as a page

showing 1–30 of 34