The Use Case Layer Published on
Onion Architecture, Clean Architecture, Hexagonal Architecture ..
They all share (more or less) the same concept: dependency inversion and structuring the application into layers (Separation of concerns).
Another thing they share, though I haven’t often seen (at least not clearly named as such) in real‑world applications, are the Use Cases. In most codebases it’s probably there, but it’s just another “service” among many, which makes it hard to tell the actual "Use Case services" apart from the rest.
To show the benefits and what a Use Case might look like, let’s look at a common example:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final ValidationService validationService;
private final UserService userService;
private final EmailService emailService;
private final NotificationService notificationService;
public UserController(
ValidationService validationService,
UserService userService,
EmailService emailService,
NotificationService notificationService
) {
this.validationService = validationService;
this.userService = userService;
this.emailService = emailService;
this.notificationService = notificationService;
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody UserCreationDto dto) {
// 0. Validate payload before any business logic
validationService.validate(dto);
// 1. Create the user record
User created = userService.createUser(dto);
// 2. Send a welcome email
emailService.sendWelcomeEmail(created.getEmail(), created.getFirstName());
// 3. Publish a “user created” notification
notificationService.publishUserCreatedEvent(created.getId());
// 4. Return 201 Created with location header
URI location = URI.create("/api/users/" + created.getId());
return ResponseEntity
.created(location)
.body(created);
}
}Nothing special here just a controller interacting with various services to create a new user. Nothing wrong with that.
Looking at different “The Clean Architecture” or other “Onion Architecture” diagrams and illustrations, we can see that there is a Use Cases layer, sometimes also referred to as “Application Services”.

The Interface Adapters layer (green) is responsible for all things HTTP/JSON/XML .... and then passing down control ... to what?
Looking back at our code example, I can't see any Use Case at all (or anything that could resemble one). All I see is services and the interactions with those, even how to save a new user? So basically this means we totally skipped this layer. The Controller knows too much.
The Use Case
I think the best description for it is the name itself. A Use Case is about what your system offers, what your system does. It’s not about a protocol or details at all—it’s as simple as saying, “My system creates users.” That’s a Use Case.
A Use Case should answer questions like, “What should happen when the user does X?”.
If we now bundle the Use Case together, we can simplify our controller to this.
@RestController
@RequestMapping("/api/users")
public class UserController {
private final CreateUserUseCase createUserUseCase;
public UserController(
CreateUserUseCase createUserUseCase,
) {
this.CreateUserUseCase = createUserUseCase;
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody UserCreationDto dto) {
User createdUser = createUserUseCase.createNewUser(mapToNewUser(dto));
URI location = URI.create("/api/users/" + createdUser.getId());
return ResponseEntity
.created(location)
.body(created);
}
}And our Use Case just contains the extracted logic from the controller before.
@Component
public class CreateUserUseCase {
private final ValidationService validationService;
private final UserService userService;
private final EmailService emailService;
private final NotificationService notificationService;
@Autowired
public CreateUserUseCase(ValidationService validationService,
UserService userService,
EmailService emailService,
NotificationService notificationService) {
this.validationService = validationService;
this.userService = userService;
this.emailService = emailService;
this.notificationService = notificationService;
}
/**
* Orchestrates the "register user" use case:
* 0. validate input
* 1. create user
* 2. send welcome email
* 3. publish user-created event
*/
public void createNewUser(CreateUserDto dto) {
validationService.validate(dto);
User created = userService.createUser(dto);
emailService.sendWelcomeEmail(
created.getEmail(),
created.getFirstName()
);
notificationService.publishUserCreatedEvent(
created.getId()
);
}In this way we can really quickly answer questions like "What does your system do?" The only thing you have to look at is your Use cases. The controller also doesn't have any detail knowledge about how to save a new user (and what needs to be done for that).
Another Approach Discovering Use Cases
Another way to discover a Use Case is through refactoring and removing duplicated knowledge.
Take the initial implementation of our UserController and imagine that you now need to implement a batch job (or whatever) creating users. We may find ourselves in a code like this.
// Reader: fetch pending rows
List<SourceRow> rows = jdbc.query(
"SELECT id, email, first_name, last_name, password FROM pending_users WHERE processed = false"
);
for (SourceRow row : rows) {
try {
// Map source row to DTO
UserCreationDto dto = new UserCreationDto();
dto.setEmail(row.getEmail());
dto.setFirstName(row.getFirstName());
dto.setLastName(row.getLastName());
dto.setPassword(row.getPassword());
dto.setSourceId(row.getId());
// 0. Validate payload before any business logic
validationService.validate(dto);
// 1. Create the user record (inline, not extracted)
User created = userService.createUser(dto);
// 2. Send a welcome email
emailService.sendWelcomeEmail(created.getEmail(), created.getFirstName());
// 3. Publish a "user created" notification
notificationService.publishUserCreatedEvent(created.getId());
// 4. Mark source row processed
jdbc.update(
"UPDATE pending_users SET processed = true, processed_at = ? WHERE id = ?",
now(), row.getId()
);
} catch (ValidationException ve) {
// mark row as invalid or move to error table
jdbc.update("UPDATE pending_users SET processed = true, error = ? WHERE id = ?", ve.getMessage(), row.getId());
} catch (Exception e) {
// record failure for retry or flag for manual review
jdbc.update("UPDATE pending_users SET last_error = ?, attempts = attempts + 1 WHERE id = ?", e.getMessage(), row.getId());
}
}One option is to instinctively extract a new service out of this duplication. We may not give naming too much attention, and we end up with a new method in a UserService (since it's about creating users). This would result in a non-distinguishable service. We may have eliminated the duplication, but we didn't gain more than that.
If we give the extraction a bit more thought, we may find ourselves surprisingly discovering a new Use Case. How you structure/name/define your Use Cases is up to everyone. In my opinion, it doesn't even have to be named after a Use Case, but to truly gain additional benefits out of it, it’s important that the Use Case services are clearly distinguishable from others.
Extending the Idea
Another idea might be to add additional meta-information to the classes/methods. In Java, for example, we could add a new custom @UseCase("This use case creates a new user") annotation. This way, I could easily differentiate and discover the Use Cases in (and also across) my system(s), and that’s information we could leverage to build tooling that enables discoverability, transparency, automation, documentation, and governance.