skip to content

In Onion Architecture, what distinguishes a domain service from an application service, and which ring does each belong to?

level: middleimportance: must knowfreq 50%

answer

  1. domain service = cross-entity business rule, pure, reusable
  2. application service = one use case, orchestrates, decides transaction boundary
  3. domain service ring is inner, application service ring is outer
  4. anemic model = logic wrongly pushed into application services
  5. test: 'what is the rule' vs 'what steps happen'

basics

~20 s

A domain service holds business rules that don't fit inside one entity, like 'does this transfer amount exceed the daily limit.' An application service coordinates a whole use case step by step, like 'validate, call the domain service, save, notify.' Domain services sit closer to the center; application services wrap around them.

solid answer

~50 s

A domain service contains business logic that is meaningful in the ubiquitous language but doesn't naturally belong to a single entity or value object — e.g., TransferPolicy.isWithinDailyLimit(account, amount) needs to reason across two accounts and a policy, so it can't live cleanly inside one Account entity. It sits in the ring immediately around the domain model, has no knowledge of transactions, persistence, or use-case sequencing, and is pure business logic. An application service, one ring further out, orchestrates a specific use case end to end: it loads entities via repository interfaces, calls domain services and entity methods, coordinates the transaction boundary, and triggers side effects like publishing an event or sending a notification. The tell: domain services answer 'what is the business rule,' application services answer 'what has to happen, in what order, to fulfill this request.' Domain services are reusable across multiple use cases; application services are typically one-per-use-case and are the layer controllers/handlers call into.

go deeper

for a junior

Should be able to give one example each of a rule that fits one entity versus one that spans several, without needing precise ring terminology.

for a middle

Should correctly place domain services one ring in from application services, explain that domain services are pure and reusable, and that application services orchestrate a specific use case including transaction boundaries.

for a senior

Should be able to spot in a real class whether logic has been misplaced (orchestration masquerading as domain logic or vice versa), and explain the anemic-domain-model failure mode that results from not making the distinction.

for a principal

Should be able to set concrete team conventions (naming, folder structure, review checklist) that keep the distinction stable as a codebase and team grow, and judge when the distinction is worth the ceremony versus when a single service layer is pragmatically fine.

## Two different kinds of business code Onion Architecture's rings between the domain model and infrastructure exist because business logic naturally splits into two different kinds of code, and conflating them is one of the most common ways teams end up with either an anemic domain model or a tangled application layer. The distinction maps closely to **Eric Evans's Domain-Driven Design** vocabulary, which Onion Architecture borrows heavily from. ## The domain service A **domain service** holds business logic that is real, meaningful business behavior — the kind of thing a domain expert would recognize and name — but that doesn't fit naturally as a method on any single entity or value object. - The classic case is **logic that spans multiple entities**: deciding whether a proposed fund transfer violates a daily transfer limit needs to look at the sending account, the receiving account, and possibly a bank-wide policy object. - It doesn't belong on `Account` because it's not really about one account's own invariants, and forcing it onto one entity produces awkward, misleading code. A domain service like `TransferLimitPolicy.evaluate(source, destination, amount)` names the rule as its own first-class concept. - Critically, a domain service is still **pure business logic**: it takes and returns domain objects, has no knowledge of a database transaction, an HTTP request, a message queue, or the sequence of steps in a use case. It sits in the ring immediately wrapping the domain model, and like the domain model itself, it should be independently unit-testable with plain objects and no infrastructure. ## The application service An **application service**, one ring further out, is a different kind of thing entirely: it represents a use case — 'transfer funds,' 'place an order,' 'cancel a subscription' — as a sequence of steps. It is the orchestrator: - it loads the entities it needs via repository interfaces; - it invokes domain-model methods and domain services to get business decisions and mutations; - it decides what to persist and when, and defines the transaction boundary; - it triggers side effects such as publishing a domain event, sending an email, or calling another bounded context. Application services are typically what a web controller, message consumer, or CLI command handler calls directly, and each one usually corresponds one-to-one with a use case rather than being reused the way a domain service is. ## The litmus test in review The core litmus test in code review is: does this method answer — | The method answers | Then it is | |---|---| | 'what is the business rule' | domain service | | 'what steps, in what order, does this specific request require' | application service | If a method contains a call to a repository's `save()`, decides transaction boundaries, or reads configuration/feature flags, it is application-layer orchestration, not domain logic, regardless of which folder it happens to sit in. ## Why bother separating them Why bother separating them rather than just having one 'services' layer? Because they change for different reasons and at different rates, exactly the same justification behind rings in general. - A **domain service** like the transfer-limit policy changes only when the actual business rule changes — new regulation, new risk appetite — and it can be reused verbatim by multiple use cases. - An **application service** changes whenever the shape of a specific workflow changes — you add a new notification step, or a new validation step — without any change to what the business rule itself actually is. Collapsing the two into one layer tends to produce two anti-patterns: 1. An **'anemic domain model'** where all logic — even pure business rules — ends up inside bloated application services because nobody bothered to identify the domain-service seam, leaving entities as dumb data bags. 2. The opposite problem, where use-case **orchestration concerns get smuggled** into what's nominally a 'domain service,' making it untestable without infrastructure and unreusable across use cases because it's secretly tied to one specific workflow's needs. ## A concrete real-world shape A concrete real-world shape of this: in a lending platform, a `LoanEligibilityService` (domain service) might encapsulate the pure rule 'given this applicant's income, existing debt, and credit score, are they eligible and at what rate' — reusable for both a new-application flow and a pre-approval-offer flow. `SubmitLoanApplicationService` (application service) is the orchestrator for the 'submit application' use case specifically: it loads the applicant, calls `LoanEligibilityService`, persists the application via a repository interface, and publishes a `LoanApplicationSubmitted` event, wiring together exactly what that one request needs to happen, in that order.

  • If a piece of logic needs to call a repository to load data before making a business decision, which ring does it belong in?
    Calling a repository is an application-service concern, because it involves orchestration — deciding what to fetch and when — not a pure business rule. A domain service can still make the decision once given the loaded objects as parameters, but it shouldn't be the one reaching into a repository itself; that keeps it free of persistence timing concerns and easy to unit test with plain in-memory objects.
  • What symptom in a codebase suggests the team has an anemic domain model caused by conflating these two service types?
    Entities that are little more than getters/setters with no behavior, combined with 'service' classes that are hundreds of lines long and contain both business rules and orchestration steps mixed together. Business logic tests end up needing mocks for repositories and transaction managers even when testing what should be a pure rule, which is a strong signal the domain-service seam was never identified.
  • Can an application service call another application service directly?
    It's generally discouraged and treated as a smell, because it usually means two use cases actually share a sub-workflow that should be extracted into its own reusable piece (a domain service, or a smaller shared application-layer helper) rather than one use-case orchestrator silently depending on and triggering another's full side effects, like duplicate event publishing or nested transaction boundaries.

A domain service is like a referee who only knows the rules of the game (is this move legal); an application service is like the match director who runs the whole event end to end — checks the referee's rulings, coordinates the players, and decides what happens next.

saying these in an interview costs you the question

  • Can't give an example of business logic that doesn't fit on a single entity
  • Thinks 'service' is a single undifferentiated layer with no distinction
  • Puts repository/transaction calls inside what they call a 'domain service'
  • Believes application services are reusable the same way domain services are
  • Doesn't recognize an anemic domain model as a symptom of misplacing logic

context