How should unit-testing strategy differ between a domain service (like a pricing calculator that applies business rules) and an application service (like a use-case orchestrator that opens a transaction and calls repositories) in a DDD-layered backend?
answer
- pure domain logic = no mocks needed
- application service tests verify call sequence
- integration test for real persistence path
- expensive tests => leaked business logic signal
- property-based testing fits stateless domain services
basics
~20 sA domain service can be tested with plain objects — call it and check the result. An application service usually needs mocked or fake repositories to test, since its job is coordinating things, not doing the business math itself.
solid answer
~40 sA domain service is pure business logic over domain objects with no infrastructure dependencies, so it's unit-tested by constructing inputs directly and asserting on outputs — no database, no mocking framework, typically just an injected stateless policy object at most. An application service's job is orchestration — repositories, authorization, transactions — so its tests either mock those collaborators to verify the sequence of calls, or run as thinner integration tests against a real or in-memory database to verify the whole use case. Because the application service shouldn't contain business rules, its tests shouldn't need to enumerate many rule edge cases — those belong in the domain object's own test suite, which is cheap because it needs no infrastructure.
go deeper
Can state that domain logic is easier to test than code touching a database.
Can describe writing domain-service tests with plain objects and application-service tests with mocked repositories.
Recognizes expensive/bloated application-service test setup as a signal of business-logic leakage and adjusts the design in response.
Sets team-wide testing conventions (ratio of fast domain unit tests to slower integration tests, when database-backed integration tests are required) informed by this layering distinction.
## Why the tests give the split away Testing strategy is one of the most practical, day-to-day signals of whether the application-service/domain-service split is actually being honored in a codebase, because the two layers have fundamentally different dependency profiles, and that difference should show up directly in how their tests are written. ## Testing a domain service A domain service, done correctly, has no infrastructure dependencies at all — it operates purely on domain objects passed in as parameters (entities, value objects) plus perhaps a constant, injected policy object that's configuration, not per-call state. This means a unit test for a domain service looks almost embarrassingly simple: - construct the input objects directly - call the method - assert on the returned value or the resulting state of the mutated objects No test-database, no HTTP mock server, no security context stub. For example, testing a `ShippingCostCalculator.calculate(order, destination)` domain service means building an `Order` with specific line items and an `Address` in a specific zone in memory, calling `calculate`, and asserting the returned amount matches the expected value for every pricing-rule branch (standard rate, bulk discount, international surcharge, free-shipping threshold). This is **fast** (milliseconds per test, no I/O), **deterministic**, and lets you exhaustively cover business-rule edge cases because there's no setup cost per test case. ## Testing an application service An application service, by contrast, has real infrastructure dependencies by design — it needs a repository to load aggregates, possibly a security/authorization component, a transaction manager, and an event publisher. Testing it well requires a different approach, and teams typically use one of two strategies, often both, for different purposes. 1. **The first is a mockist unit test**: inject mock/fake implementations of the repository and any other collaborators, call the application service method, and assert on the sequence and correctness of calls — did it call `repository.findById()` with the right ID, did it call `save()` exactly once with the expected aggregate state, did it check authorization before touching the repository at all. This kind of test is valuable for verifying orchestration logic without needing a real database. 2. **The second is a genuine integration test** — often using an in-memory or containerized database — that exercises the application service against real repository implementations, verifying the whole use case actually persists correctly, transactions actually roll back on failure, and so on. This is slower and typically run less frequently than the pure unit tests, but it's the only way to catch issues like a forgotten transaction boundary or a mapping bug between the persistence layer and the domain model. ## Where business-rule edge cases belong The key practical implication of the split, and the reason this matters beyond pure testing philosophy, is where business-rule edge cases should be exercised. If an application service's tests need to enumerate many pricing-rule branches to get confidence in the use case, that's a strong signal business logic has leaked into the application service and needs to move down into a domain object or domain service — because testing that logic through the application-service seam means dragging in a mock repository or a real database just to test what should be a pure calculation, which makes the test suite slower and more brittle than necessary. ## Test-suite rot, and the healthy ratio A concrete failure mode from getting this wrong shows up as test-suite rot: teams whose application services have absorbed business logic end up with slow test suites where every use-case test needs a database or a dozen mocks just to verify a discount calculation, and engineers respond by writing fewer edge-case tests because each one is expensive to set up — so coverage of the actual business rules quietly degrades over time. Conversely, well-separated domain logic tends to accumulate rich, cheap unit-test suites alongside a much smaller number of application-service tests that only verify orchestration plumbing, which is the healthy ratio: - business-rule correctness proven exhaustively and cheaply at the domain layer - use-case wiring proven at the application layer with a handful of representative scenarios plus a couple of true integration tests per use case ## One more nuance One more nuance worth naming: because a domain service is stateless and pure, property-based testing (generating many random valid inputs and asserting invariants hold, e.g. "total after discount is never negative") is a natural fit and cheap to run — a technique that's far harder to apply to an application service full of mocked I/O.
- If an application-service unit test needs to mock five different return values from a repository just to hit one 'successful order placement' path, what does that suggest?It suggests the application service's orchestration is either too complex for a single use case, or that business-rule branching is happening in the application service itself rather than being delegated to domain objects, since a pure orchestration script usually only needs one or two collaborators mocked.
- Why might you still want a handful of true integration tests for an application service even if all business logic has been correctly pushed into domain services?Because orchestration bugs are still real bugs — a missing transaction boundary, a mapping mismatch, a repository call in the wrong order, or an event published before commit — and these only surface when exercised against real infrastructure, not against mocks that assume the collaborator behaves correctly.
- Does a stateless domain service ever need a mock in its unit tests?Rarely, but if it's injected with a collaborator that itself needs to be a domain-layer abstraction with multiple implementations (e.g., a Specification or Policy interface), you might fake that interface with a simple in-memory implementation rather than a heavy mocking framework — the point is you're still not mocking infrastructure like databases or HTTP clients.
Testing a domain service is like taste-testing a recipe by cooking it on a countertop — you just need the ingredients. Testing an application service is like verifying a restaurant's full dinner-service workflow — you need the actual kitchen, waitstaff, and register running, because you're checking that the coordination between them works, not the recipe itself.
saying these in an interview costs you the question
- Thinks domain services need database mocks to test
- Writes application-service tests that enumerate many business-rule edge cases instead of orchestration paths
- No integration tests exist at all for application services because 'unit tests cover it'
- Doesn't notice a bloated mock setup as a signal of layering problems