Services Return, Callers Present

A service should return a result and let the caller decide how to show it — the single most useful separation-of-concerns rule.

~2 min read updated Jul 19, 2026 Clean Code
  • #clean-code
  • #architecture
  • #separation-of-concerns
  • #services

One rule that quietly fixes a lot of design problems: a service computes and returns; the caller presents. The service should not know whether its result ends up in a terminal, an HTTP response, a test assertion, or a log line.

The smell

A login service that prints its own outcome:

public class LoginService
{
    public void Login(string username, string password)
    {
        var user = Find(username, password);
        if (user is not null)
            Console.WriteLine($"Welcome, {user.Username}!");   // ← service knows about the console
        else
            Console.WriteLine("Invalid credentials.");
    }
}

This works, but the service is now welded to the console. You can't reuse it in a web API, you can't unit-test the outcome without capturing stdout, and the "what happened" is trapped inside a void.

The fix

Return the outcome. Let whoever called it decide what to do with it:

public class LoginService
{
    public User? Login(string username, string password)
    {
        return Find(username, password);   // just the result
    }
}
// The CALLER presents — and this can differ per context:
var user = loginService.Login(username, password);

if (user is not null)
    Console.WriteLine($"Welcome, {user.Username}!");   // console app
else
    Console.WriteLine("Invalid credentials.");

// A web API caller would instead: return user is not null ? Ok(user) : Unauthorized();
If the outcome is richer than "found / not found" — say you need a reason for the failure — return a small result object instead of null, rather than reaching for the console:
public record LoginResult(bool Success, User? User, string? Error);

Why it pays off

  • Reusable — the same service serves a console app, an API, and a background job.
  • Testable — you assert on a returned value, not on captured output.
  • Honest signatures — a method that returns User? tells you what it does; a void that secretly prints does not.

It's the same rule you already follow

In Laravel you don't echo from a service — the service returns data and the controller turns it into a response. Same instinct, same payoff. The console is just this project's "view".

See the C# services note for the full example in context → Services & helpers.

Related notes