skip to content

questions

22

In a layered application, what does it mean for source-level dependencies to 'point inward', and why does this matter for the codebase's design?

level: juniorimportance: must knowfreq 65%

answer

  1. arrows point inward
  2. domain doesn't import framework
  3. stable core, volatile edges
  4. swap infra without touching logic

basics

~20 s

Code in the core business logic should not need to know about outer layers like databases or web frameworks. Only the outer layers should import and depend on the inner ones, never the other way around.

solid answer

~30 s

'Pointing inward' means the import/reference arrows in your source code all flow from outer, detail-heavy layers (UI, database, frameworks) toward the inner, stable layer (business rules/domain). The domain package never imports a database driver or a web-framework class; instead, the database adapter imports the domain's interfaces and implements them. This keeps the core logic testable and framework-agnostic, and it lets you swap infrastructure (e.g., switch database or HTTP framework) without touching business rules, because change flows toward the layers designed to absorb it.

go deeper

for a junior

Should recognize the pattern name and give the 'outer depends on inner' summary with one concrete example (e.g., domain shouldn't import a web framework).

for a middle

Should be able to point to specific package examples and explain how dependency injection wires the runtime call in the opposite direction from the compile dependency.

for a senior

Connects it to the Dependency Inversion Principle and Stable Dependencies idea, discusses cost/benefit, and knows when it's worth it.

for a principal

Discusses this as an architectural policy enforced across teams/modules, ties it to long-term evolvability, and knows the failure modes at scale (boundary erosion, big ball of mud).

## What the arrows mean **"Dependency direction"** describes which way the compile-time references point between the parts of a codebase — literally, which files `import` or reference which other files. In a well-structured application you can draw the packages as concentric rings: - an **inner ring** holding business rules and domain models - a **middle ring** holding application/use-case orchestration - an **outer ring** holding technical details — web controllers, database access code, message-queue clients, third-party SDKs **"Pointing inward"** means every arrow you'd draw for an import statement goes from an outer ring toward an inner one, never the reverse. A controller class may import a use-case class; a use-case class may import a domain interface; but the domain package itself imports nothing from the controller or database packages. If you inspect the domain package's import statements and find `org.springframework` or `javax.persistence` or an HTTP client library, dependency direction has been violated, no matter how good the underlying business logic is. ## Why the rule exists This rule exists because it encodes a bet about what changes and what doesn't. | Stays put | Moves | |---|---| | Business rules — how a loan is approved, how a cart total is computed — tend to be relatively stable and are the reason the software exists. | Frameworks, databases, and external APIs are comparatively volatile: they get upgraded, replaced, or swapped for a competitor's product for reasons that have nothing to do with the business rules. | The **Dependency Inversion Principle** formalizes this as "depend on abstractions, not concretions," and the related **Stable Dependencies** idea adds the corollary that things should depend in the direction of increasing stability. If the domain package depends on the database package, then every time the database technology changes, the change pressure flows into the one part of the system you most want to protect. If instead the database package depends on interfaces defined by the domain, the database can be swapped, mocked, or reimplemented and the business rules never need to be touched, recompiled for a different reason, or even re-read. ## How the inversion works The mechanism that makes this possible despite runtime control flow going the "wrong way" is **dependency injection through an interface owned by the inner layer**. Say the `orders` use case needs to persist an order. 1. Instead of importing a `JpaOrderRepository` class directly, the use-case package declares an `OrderRepository` interface expressing only what it needs (`save(order)`, `findById(id)`). 2. The persistence package then depends on the use-case package to implement that interface with `JpaOrderRepository`. 3. At runtime, something at the edge of the system — a configuration class, a manual factory, a DI container — wires the concrete `JpaOrderRepository` into the use case. So calls still flow outward (the use case ends up invoking JPA code indirectly), but the source dependency points inward: the use-case package has zero import statements referencing JPA. ## The cost The cost of this discipline is real and worth naming honestly. Every port you introduce is: - an extra file - an extra layer of indirection - often an extra mapping step between the domain's shape and the outer layer's shape For a small CRUD app with one database and no plausible reason to swap it, this ceremony can slow a team down for no payoff — the abstraction has a cost but no realized benefit if nothing ever plugs in besides the one implementation. Teams need to weigh the likelihood of actually needing to substitute an implementation against the tax of the extra indirection on every change. ## Failure modes Failure modes show up gradually rather than as a single build error, because a single misplaced import doesn't break anything today. - The **classic symptom** is that unit-testing the core business logic becomes impossible without spinning up a real database, an application context, or a mocked HTTP server, because the "unit" under test transitively pulls in the framework. - **Another symptom** is that a seemingly small technology decision — upgrading an ORM, switching a payment provider — turns into a sprawling refactor that touches domain classes nobody expected to change. - **At the extreme**, enough inward-pointing violations accumulate that the dependency graph has cycles between what were meant to be layers, and the codebase degrades into what's colloquially called a "big ball of mud," where no part can be understood, tested, or changed in isolation. ## Ports and adapters A concrete, widely used embodiment of this idea is **Hexagonal (Ports and Adapters) Architecture**: the domain/application core defines "ports" (interfaces) for everything it needs from the outside world, and "adapters" — a REST controller, a JPA repository, a message consumer — live in outer packages and implement or call those ports. The core package can be compiled, tested, and reasoned about with zero framework dependencies on the classpath at all.

  • What's a concrete code smell that signals dependency direction has been violated in a backend codebase?
    A domain class annotated with a persistence-framework annotation, or extending a JPA base class, or a domain interface method that takes a servlet request type — these show the core layer directly referencing framework types, meaning outer-layer types have leaked inward and can't be tested or swapped without dragging the framework with them.
  • How does dependency direction differ from data flow / call flow direction?
    Call flow can be bidirectional — infrastructure calls into the domain to invoke a use case, and the domain 'calls' infrastructure indirectly through an interface it defines. Compile-time dependency (import) direction stays inward even when runtime control flow moves outward, because the interface implementation is injected, not directly referenced.
  • Does 'dependency direction' apply only to layers, or also across features/modules?
    It applies to any dependency boundary, not just horizontal layers — feature modules should also avoid one module's core logic depending on another module's implementation details; the appropriate abstraction (a port/API) is owned by whichever side is meant to be stable.

Think of it like a restaurant kitchen: the head chef's recipe (business logic) shouldn't need to know which brand of oven is installed; the oven (infrastructure) is built to fit the recipe's requirements (a port), not the other way around.

saying these in an interview costs you the question

  • Says 'just import the DB class directly, it's simpler'
  • Can't explain why unit tests need mocking/DB spin-up when direction is violated
  • Confuses call/runtime direction with source dependency direction
  • Thinks placing an interface always in the same package as its implementation is fine regardless of who calls it

context

open as a page

In Java, what's the difference between declaring a class `public` versus leaving it with default (package-private) access, and why would a team choose package-private for an internal helper class?

level: juniorimportance: must knowfreq 65%

basics

~20 s

Public means any code anywhere in the program can use the class. Default/package-private means only code living in the same folder (package) can use it. Teams hide helper classes as package-private so other parts of the codebase can't accidentally depend on internal details that might change.

open as a page

In a multi-module project, what does it mean to separate a module into a public 'API' part and a hidden 'implementation' part, and why would a team bother doing that instead of just putting everything in one module?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Split the module into two: a small public part other code depends on (the API), and a bigger hidden part with the real logic (the implementation). This lets you change the hidden part freely without breaking dependents.

open as a page

What is the difference between organizing a codebase's packages by technical layer (e.g. separate `controller`, `service`, and `repository` packages) versus organizing them by feature (e.g. a package per business capability like `orders` or `users`)?

level: juniorimportance: must knowfreq 85%

basics

~20 s

Layer-based grouping sorts files by their technical role (all controllers together, all services together). Feature-based grouping sorts files by what business thing they do (everything about orders lives in one folder, everything about users in another).

open as a page

In a classic three-tier layout where a `controller` package depends on a `service` package, which in turn directly imports concrete classes from a `repository` package that wraps the database, the code compiles and runs fine. Why is this still considered a dependency-direction problem, and how would you restructure the packages to fix it?

level: middleimportance: must knowfreq 80%

basics

~20 s

It compiles, but the business-logic layer is now stuck to one specific way of talking to the database — you can't swap it or test the logic without the real database. Fix: put an interface next to the business logic and make the database code implement that interface instead of the other way around.

open as a page

Kotlin adds an `internal` visibility modifier that Java doesn't have as a keyword. What is `internal` scoped to, and how does that differ from Java's package-private default access?

level: middleimportance: must knowfreq 55%

basics

~20 s

In Kotlin, internal means 'visible anywhere inside this compiled module (like one Gradle project or JAR), but hidden outside it.' Java's package-private is scoped to a single package folder instead, which is a much smaller boundary.

open as a page

In a Gradle multi-module Kotlin or Java build, what's the difference between declaring a dependency with the api configuration versus the implementation configuration, and how does that difference help enforce API and implementation separation?

level: middleimportance: must knowfreq 65%

basics

~20 s

api means 'this dependency is part of what I expose,' so it shows up on their build too. implementation means 'I use this internally,' so it stays hidden - consumers can't see or accidentally depend on it.

open as a page

Under package-by-layer organization (separate `controllers/`, `services/`, `repositories/` packages each containing classes for every feature), how many files and packages typically need to change to add a new `discountCode` field to an `Order`, and how does that compare under package-by-feature organization (a single `orders/` package holding `OrderController`, `OrderService`, and `OrderRepository`)?

level: middleimportance: must knowfreq 80%

basics

~20 s

With layer folders, adding one field touches files spread across several separate top-level folders (controller, service, repo). With feature folders, all those files sit together in one orders folder, so the change stays in one place.

open as a page

What concrete costs does strictly enforcing inward-pointing dependencies (ports/interfaces for every outward call) impose on a codebase, and under what circumstances would you deliberately not enforce it?

level: seniorimportance: must knowfreq 60%

basics

~20 s

Every interface you add 'just in case' is extra code to write, read, and navigate, and it can slow a small team down for no real benefit if there's genuinely only ever going to be one implementation. Skip the strict discipline for small, short-lived, or truly single-technology apps where the 'swap' you're protecting against will never happen.

open as a page

A team wants to guarantee that classes in a controller package never call classes in a repository package directly, skipping the service layer, and that this is caught automatically if someone violates it. Visibility modifiers alone can't express this since both packages are legitimately public within the same module. How would an ArchUnit rule enforce this, and why is a runnable test needed here instead of relying on code review?

level: seniorimportance: must knowfreq 50%

basics

~20 s

ArchUnit is a testing library that reads your compiled Java/Kotlin code and lets you write rules like 'classes in the controller package must not access classes in the repository package.' You put that rule in a normal unit test, so if someone writes code that breaks the layering, the test fails and the build breaks — catching it automatically instead of hoping a reviewer notices.

open as a page

How does the Java Platform Module System (JPMS), introduced in Java 9, enforce API versus implementation separation at the language and runtime level, and how is that different from just relying on the public access modifier?

level: seniorimportance: must knowfreq 45%

basics

~20 s

JPMS lets a module list, in a module-info.java file, exactly which packages it exports. A public class in a non-exported package is completely invisible outside the module - the JVM blocks access, not just a naming convention.

open as a page

How does package-by-feature versus package-by-layer affect coupling and cohesion at the package level, and are there situations where you'd deliberately choose package-by-layer despite its locality-of-change downsides?

level: seniorimportance: must knowfreq 70%

basics

~20 s

Feature packages keep related code together (high cohesion) and keep unrelated features apart (low coupling between packages). Layer packages do the opposite - unrelated features get lumped together, while each feature's own code is spread thin. Layer-based still makes sense for very small apps or when the tech layers themselves are the reusable, shared thing, like a generic data-access library.

open as a page

Two feature modules, `orders` and `inventory`, are each internally well-layered — inside each, controllers depend on services depend on domain, all pointing inward. But `orders`' domain package imports a class from `inventory`'s domain package to check stock, and separately `inventory`'s domain package imports a class from `orders`' domain package to look up order history. What problem does this create, and how would you fix it while keeping both modules internally clean?

level: middleimportance: should knowfreq 55%

basics

~20 s

Even though each module looks fine inside itself, the two modules now depend on each other in both directions, so you can't understand, build, or test one without the other. Fix: pick which module is allowed to know about the other, or use an event so neither core has to import the other's internals directly.

open as a page

Before Java 9, if a library's JAR was on the classpath, every public class in every package it contained was callable by consumers — including 'internal' implementation packages the library author never intended to expose (a real historical example: sun.misc.Unsafe). What does the Java Platform Module System (JPMS) change with module-info.java, and what do the exports and opens directives actually control?

level: middleimportance: should knowfreq 35%

basics

~20 s

JPMS lets a JAR declare, in a module-info.java file, exactly which packages it shares with the outside world using exports. Any public class in a package NOT listed stays hidden from other modules even though it's technically public, so the compiler and the JVM both block access from outside.

open as a page

What does 'screaming architecture' (a term popularized by Robert C. Martin) mean, and how does choosing package-by-feature over package-by-layer support it?

level: middleimportance: should knowfreq 55%

basics

~20 s

It means a codebase's top-level folder names should tell you what business the software does, not what framework or technical pattern it uses. Feature folders like orders and shipping do that; layer folders like controllers and services don't.

open as a page

A codebase started with clean inward-pointing dependencies, but eighteen months later a code review finds a domain class importing a REST client class from the outer web layer. Nothing in the build pipeline caught it before merge. What allowed this drift to happen, and what would you put in place to catch it automatically going forward?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Nobody was checking; the compiler happily allows any import as long as types resolve, so 'clean architecture' without an automated check is just a convention people can forget under deadline pressure. Fix: add an automated test that fails the build if a package in the inner layer imports anything from an outer-layer package.

open as a page

Many codebases use a naming convention — an internal sub-package, which Go formalizes as a compiler-enforced rule for any package under a directory literally named internal/, or an impl/internal suffix — to mark implementation packages that outside code shouldn't import. In a language like Java or Kotlin, where the compiler does NOT understand this convention the way Go's does, why do teams still use it, and what has to back it up for the boundary to actually hold?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Naming a package internal tells other developers 'don't import this' even though the compiler won't stop them — Java and Kotlin, unlike Go, don't give that folder name any special meaning. For the rule to actually be enforced, teams add automated checks like ArchUnit tests that fail the build if something outside the boundary imports from an internal package.

open as a page

When designing a module's public API surface, what's the practical process for deciding what goes in the API versus what stays in the implementation, and what goes wrong when a team just exposes everything 'to be safe'?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Only put in the API the smallest set of types and methods other code actually needs. Everything else stays hidden. Exposing extra stuff 'just in case' makes it much harder to change later, because someone eventually relies on it.

open as a page

A platform team is choosing how to enforce module boundaries across a large codebase: JVM visibility modifiers, compile-time; an architecture-test library like ArchUnit or Spring Modulith's ApplicationModules.verify(), test-time static analysis; or the Java Platform Module System, JPMS, runtime strong encapsulation. What does each actually guarantee, at what point in the pipeline does each one fail loudly, and what's the cost of over-relying on any single one?

level: principalimportance: should knowfreq 25%

basics

~30 s

Visibility modifiers stop bad code from even compiling, but only within one package or module. Architecture tests catch bigger-picture rule violations, like 'don't call the database from the web layer,' when you run your test suite. JPMS enforces boundaries at runtime for the whole application, even against tricks like reflection, but it's the most work to set up. Most teams use compiler visibility plus architecture tests, and skip JPMS unless they really need its strength.

open as a page

At an organization with dozens of teams sharing internal library modules, how do you keep the api/implementation split from eroding over time, and what tooling or process actually enforces that a published api module's contract doesn't silently break its consumers?

level: principalimportance: should knowfreq 30%

basics

~20 s

You need automated checks, not just good intentions - tools comparing a new API module version's bytecode against the previous one, flagging anything that would break callers, plus a versioning rule enforcing the intended split.

open as a page

You've inherited a large, multi-year-old Spring backend organized strictly package-by-layer (`controllers/`, `services/`, `repositories/`, roughly 40 classes in each), with several teams actively shipping features into it. How would you approach migrating it toward package-by-feature, and what structure would you land on internally within each feature package?

level: principalimportance: should knowfreq 40%

basics

~20 s

Don't do a giant rewrite. Move one feature at a time into its own folder while the app keeps running, starting with the feature that changes most often. Inside each feature folder, still keep some internal grouping (like controller/service/repo) so it's not one giant flat pile of files.

open as a page

Beyond an ArchUnit-style test that checks package dependency direction within one compilation unit, some teams split layers into physically separate build modules (e.g., separate Gradle modules or JARs) with explicit `dependencies {}` declarations. What additional guarantee does a physical module boundary provide that a same-source-tree architecture test does not, and what does it cost?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

A test only catches a wrong-direction import when someone runs the test suite. A separate build module makes it flat-out impossible to compile a wrong-direction import in the first place, because the compiler can't even see the other module's code unless it's declared as a dependency.

open as a page