📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What does Tell, Don't Ask mean, and how does it change the way you design domain classes in C#?

Tell objects what to do instead of querying state and deciding outside; anaemic versus rich models; invariants; where querying is fine (CQRS reads, DTOs).

Mid-Level Theory Intermediate design-principles domain-model encapsulation tell-dont-ask

🎯 Key points

  • Tell an object to do something rather than asking for its state and deciding for it
  • Keeps behaviour with the data so invariants are enforced in one place
  • Ask-then-act code in services leads to anaemic models and duplicated rules
  • Methods like order.Cancel() or account.Withdraw() replace external status checks
  • Querying is fine for read models, reporting and UI display
  • Return results or throw domain exceptions when a command is not allowed

⏱ 30-second answer

Tell, Don't Ask means you tell an object what you want done, and the object decides how, using its own data. Instead of a service doing if (order.Status == Pending && !order.IsPaid) order.Status = Cancelled;, you call order.Cancel(), and the order enforces the rule. That keeps invariants in one place, removes duplicated checks across services and moves you from an anaemic model towards a rich one. It applies to behaviour; for reading data to display or report, querying is exactly right.

🗣 Full answer (senior)

How I would answer this in an interview

"Tell, Don't Ask means you tell an object what you want done and let it decide how, using its own data, instead of asking for its state and making the decision outside. The ask style looks like if (order.Status == Pending && !order.IsShipped) order.Status = Cancelled; sitting in a service. Every caller has to remember every rule, and one that forgets corrupts state. The tell style is order.Cancel(), and the order enforces its own rules: business logic lives with the data it protects. That keeps invariants in one place, removes duplicated checks across services, makes rules unit-testable on the entity itself, and moves a codebase from an anaemic model (property bags plus services) towards a rich model. In C# I enforce it with private setters, methods named after domain actions (Approve, Ship, Cancel, Withdraw) and a result type or domain exception when an action isn't allowed. EF Core maps private setters and backing fields happily. It applies to behaviour: for reading data to display, report or return from an API, asking is exactly right."


1. Ask vs tell

Ask (logic outside) Tell (logic inside)
if (account.Balance >= amount && !account.IsFrozen) account.Balance -= amount; account.Withdraw(amount);
if (order.Status == Pending) order.Status = Cancelled; order.Cancel();
if (subscription.EndsAt < now) subscription.IsActive = false; subscription.ExpireIfDue(now);
Rule duplicated in every caller Rule in one place
Invariants can be bypassed via setters State changes only through methods

2. Benefits

Benefit Why
Invariants enforced once Only the object changes its own state
No duplicated rules Services call the method instead of re-implementing checks
Easy unit tests Test Order transitions without services, databases or mocks
Safer refactoring Internal representation can change behind the methods
Clearer language Code speaks the domain (Cancel, Ship) instead of setting flags
Fewer Demeter violations Callers stop navigating state

3. Where asking is right

Situation Why asking is fine
UI screens, reports, API responses They need data, not behaviour
Read models / CQRS queries Purpose-built for querying
DTOs across boundaries Plain data
Decisions spanning several aggregates A domain service asks each, then tells each
Simple CRUD with no rules A property bag is honest when there's no behaviour to protect

4. Making it stick in C#

Technique Purpose
private set / backing fields No external state changes
Read-only collections exposed (IReadOnlyList<OrderLine>) No external Add bypassing rules
Action methods (AddLine, Ship, Cancel) The only way to change state
Result types or domain exceptions Report invalid actions explicitly
Factory methods / constructors with validation Objects start valid
Command-query separation Commands change state; queries return data

Production example

In our [order platform], [five] services could cancel an order (customer self-service, support tool, fraud job, payment-failure handler and an admin API), each with its own status checks, and Order.Status had a public setter. The fraud job's check forgot that shipped orders can't be cancelled, and [hundreds] of shipped orders were cancelled and refunded before anyone noticed. I moved the rule into Order.Cancel() (returning a failure result for shipped orders, idempotent for already-cancelled ones), made the setter private, and changed all five services to call the method. EF Core kept mapping the private setter without changes. We added unit tests for every state transition and a review rule against public setters on aggregates; there's now exactly one way to cancel, and it can't skip the check.

🔍 What the interviewer is checking

Checks domain modelling and encapsulation skills.

Green flags

  • Shows ask versus tell code.
  • Connects it to invariants and anaemic models.
  • Knows where asking is appropriate.
  • Mentions private setters working with EF Core.

Red flags

  • All entities are property bags with logic in services, with no awareness of the trade-off.
  • Applies it dogmatically to DTOs.

📚 Learn in depth

1. Origin and relatives

Idea Source Relationship
Tell, Don't Ask Andy Hunt & Dave Thomas (Pragmatic Programmers); Alec Sharp Put behaviour with data
Command-Query Separation Bertrand Meyer Commands change state, queries return data
Law of Demeter Holland & Lieberherr Telling the nearest object removes navigation chains
Rich domain model / aggregates Eric Evans (DDD) Aggregates enforce their invariants through methods
Anaemic domain model (anti-pattern) Martin Fowler Data-only entities with logic in services

2. A state machine as the entity

          MarkPaid()          Ship()
Pending ------------> Paid ----------> Shipped
   |                    |
   | Cancel()           | Cancel()
   v                    v
 Cancelled <------------+
(Shipped → Cancel() = failure)

Each transition is a method; illegal transitions are rejected in one place. Tests cover the table of transitions directly.

3. Collections without bypasses

public sealed class Order
{
    private readonly List<OrderLine> _lines = [];
    public IReadOnlyList<OrderLine> Lines => _lines;

    public void AddLine(Sku sku, int quantity, Money unitPrice)
    {
        if (Status != OrderStatus.Pending) throw new InvalidOperationException("Order is locked");
        if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
        _lines.Add(new OrderLine(sku, quantity, unitPrice));
    }
}

EF Core maps _lines via the backing field (builder.Navigation(o => o.Lines).UsePropertyAccessMode(PropertyAccessMode.Field)).

4. Result types vs exceptions

Use When
Result type (Result.Fail("...")) Expected business outcomes callers should handle ("shipped orders can't be cancelled")
Domain exception Programming errors or invariant violations that shouldn't happen if callers behave

Either way, the decision is made inside the entity.

5. Cross-aggregate decisions

public sealed class TransferService
{
    public Result Transfer(Account from, Account to, Money amount)
    {
        var withdrawn = from.Withdraw(amount);   // tell
        if (!withdrawn.IsSuccess) return withdrawn;
        to.Deposit(amount);                      // tell
        return Result.Ok();
    }
}

The service coordinates; each account still enforces its own rules.

6. Interfaces for behaviour

When several types share a behaviour contract (e.g. ICancellable for orders and subscriptions), an interface lets callers "tell" without knowing the concrete type. Keep it small (ISP) and behaviour-shaped (Cancel()), not state-shaped (Status { get; set; }).

7. Try it yourself

  1. Find a service that sets an entity's Status directly.
  2. Add an action method on the entity with the rule, and make the setter private.
  3. Build and fix every compile error by calling the method; count how many duplicated checks disappear.
  4. Write transition tests for the entity (one per allowed/denied transition).
  5. Expose a collection as IReadOnlyList<T> with an AddX method and map the backing field in EF Core.

💻 Code

var order = new Order(Guid.NewGuid());
order.MarkPaid();
order.Ship();

var result = order.Cancel();
Console.WriteLine(result.IsSuccess ? "Cancelled" : result.Error); // Shipped orders cannot be cancelled

public enum OrderStatus { Pending, Paid, Shipped, Cancelled }

public readonly record struct Result(bool IsSuccess, string? Error)
{
    public static Result Ok() => new(true, null);
    public static Result Fail(string error) => new(false, error);
}

public sealed class Order(Guid id)
{
    public Guid Id { get; } = id;
    public OrderStatus Status { get; private set; } = OrderStatus.Pending; // no external writes

    public void MarkPaid()
    {
        if (Status != OrderStatus.Pending) throw new InvalidOperationException("Only pending orders can be paid");
        Status = OrderStatus.Paid;
    }

    public void Ship()
    {
        if (Status != OrderStatus.Paid) throw new InvalidOperationException("Only paid orders can ship");
        Status = OrderStatus.Shipped;
    }

    // Tell: the order owns the cancellation rule.
    public Result Cancel()
    {
        if (Status is OrderStatus.Shipped) return Result.Fail("Shipped orders cannot be cancelled");
        if (Status is OrderStatus.Cancelled) return Result.Ok();
        Status = OrderStatus.Cancelled;
        return Result.Ok();
    }
}

⚠ Common mistakes

1. Public setters on entities with rules

What happens: order.Status = OrderStatus.Cancelled from anywhere. Why it is wrong: any code can put the object in an invalid state. Do instead: private setters and action methods that enforce rules.

2. Applying it to read models and reports

What happens: methods added to hide data needed for display. Why it is wrong: screens and reports need data; hiding it adds noise. Do instead: plain query models for reads.

3. Cross-aggregate decisions inside one entity

What happens: Order.Cancel() loads and modifies inventory and payments. Why it is wrong: the entity couples to other aggregates and their loading. Do instead: a domain/application service that tells each aggregate.

4. Methods that change state and return unrelated data

What happens: Ship() returns a list of customer emails. Why it is wrong: breaks command-query separation; surprises callers. Do instead: commands return success/failure; queries return data.

5. Exposing mutable collections

What happens: public List<OrderLine> Lines { get; }. Why it is wrong: callers Add directly and bypass validation. Do instead: IReadOnlyList<T> plus action methods.

6. Renaming setters without adding rules

What happens: SetStatus(OrderStatus s) that just assigns. Why it is wrong: same bypass, different name. Do instead: domain actions (Cancel, Ship) that encode allowed transitions.

7. Believing EF Core requires public setters

What happens: rich models avoided "because of the ORM". Why it is wrong: EF Core maps private setters, backing fields and constructors. Do instead: use field access mode and parameterised constructors.

8. Putting every rule on entities

What happens: entities call repositories, email services and APIs. Why it is wrong: entities take on infrastructure concerns. Do instead: entities hold rules about their own state; services coordinate I/O.

9. Silent failures

What happens: Cancel() does nothing for shipped orders and returns void. Why it is wrong: callers think it worked. Do instead: return a result or throw so callers must handle it.

10. Dogmatism for simple CRUD

What happens: rich-model ceremony for entities with no rules. Why it is wrong: adds code without protecting anything. Do instead: apply it where there are invariants to protect.

🏛 Senior insight: trade-offs

I use Tell, Don't Ask to decide where a rule lives. If several services check an entity's state before changing it, the rule belongs on the entity.

Code pattern Move to
if (x.Status == ...) x.Status = ... in services Entity method
Same check in several handlers Entity or value object
Rule across two aggregates Domain service
Screen needs fields Query/read model (asking is fine)

🔗 Follow-ups the interviewer may ask

  1. When: Candidate contrasts anaemic and rich models Why: Probes domain modelling depth
    How to answer

    In an anemic domain model, entities are property bags with public getters and setters, and all business rules live in service classes that read and set those properties. In a rich domain model, entities own their behaviour: state is private, changes happen through methods like order.Pay(amount) that enforce invariants, and services only orchestrate (load, call, save). Anemic models scatter and duplicate rules, let any code put objects into invalid states, and are hard to test without a database. They are acceptable for simple CRUD or reporting with few rules. Move towards a richer model incrementally: make setters private, move one rule at a time into the entity, protect collections, and use value objects, guided by where bugs actually occur.

  2. When: Candidate removes property chains from services Why: Links to the Law of Demeter
    How to answer

    The Law of Demeter (principle of least knowledge) says a method should only call methods on: itself, its parameters, objects it creates, and its own fields. A chain like order.Customer.Address.Postcode.StartsWith("6") couples the caller to the structure of four types, so changing any of them breaks it. The fix is usually to ask the nearest object for what you need (order.ShipsToWa()), which moves behaviour to where the data lives. It is about navigating object structures, not counting dots: LINQ and fluent builders return the same abstraction each call, and plain DTOs are data, so neither is a violation.

  3. When: Candidate gives entities their own rules Why: Checks SRP balance on entities
    How to answer

    SRP says a module should have one reason to change, which Robert C. Martin later phrased as "responsible to one actor": one group of people whose requests drive its changes. It is not "a class does one thing" or "one method per class". In .NET I look for classes that change for unrelated reasons, such as an OrderService that holds pricing rules (finance), email templates (marketing) and SQL (DBA), and split along those change drivers. I stop splitting when the pieces would always change together, because that is just scattering one responsibility across files.

🛠 Real-world problems: how you handle them

Shipped orders refunded by a cancellation service
Situation
[Five] services could cancel orders, each checking status itself.
Signals
[Hundreds] of refunds issued for orders that had already shipped.
Investigation
One service's check omitted the shipped status; the entity had a public `Status` setter.
Resolution
Added `Order.Cancel()` enforcing all rules; made the setter private; all services call the method.
Prevention
Entity unit tests for state transitions; analyser or review rule against public setters on aggregates.

How to tell it (STAR)

Five different services could cancel orders and one forgot that shipped orders cannot be cancelled, so we refunded [hundreds] of shipped orders. I moved the rule into a `Cancel` method on the order and made the status setter private. Now there is only one way to cancel, and it cannot skip the check.

↩ Usually asked after

More in Additional Principles

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me