Devlog: User Registration Domain Refactoring
The “DTO Leak” That Bugged Me

Date: 2026-01-20
PRs: #223, #224, #225, #226, #227
Theme: Domain-Driven Design · Command Pattern · DTO Refinement
Author: leodvincci
Branch: refactor/user-registration-domain → main
Window: Jan 20, 2026 (18:46–22:36) — ~3h 44m
Context: The “DTO Leak” That Bugged Me
My user registration module needed some architectural tightening.
The core issue was simple (and painfully common): my service layer was operating directly on web DTOs. That meant my domain logic was coupled to my API contract—exactly the kind of boundary leak that feels fine… until the day you want to evolve your API, add a new interface (CLI, batch job), or test the domain without HTTP baggage.
So today’s mission:
Keep DTOs at the boundary. Let the domain speak its own language.
That language is:
Commands (what we ask the domain to do)
Results (what the domain returns, intentionally shaped)
Entities (domain internals, not an API output format)
This refactor shipped as five PRs, merged in small, safe steps, with 13 commits total (including merge commits), all within about four hours.
The Big Idea
Before:
Controller accepts DTO
Service takes DTO (domain depends on web)
Service returns Entity (web sees internals)
After:
Controller accepts Request DTO
Controller maps DTO → Command
Service takes Command (domain language)
Service returns Result (focused contract)
Controller maps Result → Response DTO
The mapper becomes a translator and a guardrail—an anti-corruption layer between the web and the domain.
Phase 1: Introduce the Command Pattern (PR #223)
Commit: 4c1cb4a — Add UserRegistrationRequestCommand
Added
UserRegistrationRequestCommandrecord in the domain layerMapper methods in
UserRegistrationMapper
Modified
UserRegistrationServicenow accepts a command instead of a DTOUserRegistrationControllernow depends on the mapper
Files changed: 4 files, +25 / -6
PR #223 merged: Tue Jan 20 18:52:17 2026
What Changed (The Leak Gets Plugged)
Before: service depended on a web DTO.
public class UserRegistrationService {
public UserEntity register(UserRegistrationRequestDTO dto) {
// Service directly depends on web DTO
}
}
After: service depends on a domain command.
public class UserRegistrationService {
public UserEntity register(UserRegistrationRequestCommand command) {
// Service works with domain command
}
}
And the controller became responsible for translating boundary → domain:
@RestController
public class UserRegistrationController {
private final UserRegistrationMapper mapper;
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody UserRegistrationRequestDTO dto) {
UserRegistrationRequestCommand command = mapper.toCommand(dto);
UserEntity result = service.register(command);
return ResponseEntity.ok(mapper.toDTO(result));
}
}
Why This Matters
Decoupling: domain no longer knows what the API looks like
Testability: service can be unit-tested without DTOs
Flexibility: DTOs can evolve independently of domain logic
Phase 2: DTO Naming Consistency (PR #224)
Commit: 2363ded — Rename Registration DTOs
Renamed
UserRegistrationRequestDTO→RegisterUserRequestDTOUserRegistrationResponseDTO→RegisterUserResponseDTO
Modified
Mapper method signatures
Controller references
Files changed: 4 files, +15 / -15
PR #224 merged: Tue Jan 20 20:29:58 2026
The Naming Upgrade
The old names were verbose and didn’t match the patterns I’m using elsewhere. The new naming follows a verb-first convention:
| Old | New | Pattern |
UserRegistrationRequestDTO | RegisterUserRequestDTO | [Action][Resource][Type] |
UserRegistrationResponseDTO | RegisterUserResponseDTO | [Action][Resource][Type] |
This lines up with command naming like CreateBookCommand, CheckoutBookCommand, etc.
Naming is architecture. It shapes what code wants to become.
Phase 3: Refine Commands + Add Result Objects (PR #225)
Commit: dc9464c — Replace Command and Add Result Object
Added
RegisterUserCommand(replacingUserRegistrationRequestCommand)RegisterUserResultrecord to encapsulate service response
Renamed
UserRegistrationMapper→AppUserMapper
Modified
Service now returns
RegisterUserResultResponse DTO enhanced to include user ID
Files changed: 7 files, +31 / -22
PR #225 merged: Tue Jan 20 22:03:50 2026
The Full Flow (Now Clean)
Request DTO → Command → Service → Result → Response DTO
Web Layer Domain Layer Domain Layer Web Layer
DTO → Command → Service → Result → DTO
Before vs After
Before: Service returned the entity directly.
UserEntity result = service.register(command);
RegisterUserResponseDTO response = mapper.toResponseDTO(result);
After: Service returns a result object.
RegisterUserResult result = service.register(command);
RegisterUserResponseDTO response = mapper.toResponseDTO(result);
Why Result Objects?
Because returning entities is like giving your API consumers a “free tour” of your internals.
| Concern | Without Result | With Result |
| Return value | Entity | Focused result |
| Coupling | Exposes full entity | Hides internals |
| Evolution | Entity changes can break API | Result buffers change |
| Clarity | Purpose unclear | Intent explicit |
The result record contains only what the outside world needs:
public record RegisterUserResult(
Long userId,
String username,
String email
) {}
Why Rename the Mapper?
UserRegistrationMapper became AppUserMapper because it maps across:
DTO ↔ Command
Result ↔ Response DTO
Command ↔ Entity (domain construction)
“Registration” was too narrow. “AppUser” is the actual domain concept.
Phase 3.1: Clean Up Dead Mapper Code
Commit: f030d9c — Remove Unused Mapper Method
Removed a dead method: toDTO(RegisterUserCommand).
Commands flow into the domain. They don’t flow back out.
✅ Correct flow: DTO → Command → Service
❌ Wrong flow: Command → DTO (why would we ever do that?)
Dead code removal isn’t glamorous, but it keeps the architecture legible.
Phase 3.2: Test Enhancements
Commit: 209e53d — Formatting and Test Enhancement
Updated the controller test to assert more than “it worked.”
@Test
void testRegisterUser() {
// ... existing setup ...
assertNotNull(response.getUserId());
assertEquals("testuser", response.getUsername());
assertEquals("test@example.com", response.getEmail());
}
Now the test verifies the full mapping chain and the improved response contract.
Phase 4: Complete the Mapping Chain (PR #226)
Commit: e0a007b — Add toResponseDTO(RegisterUserResult)
Added
toResponseDTO(RegisterUserResult)method inAppUserMapper
Modified
Controller now uses mapper for all conversions
Added structured logging
Files changed: 2 files, +8 / -3
PR #226 merged: Tue Jan 20 22:21:27 2026
This removed manual response construction in the controller and centralized transformation logic inside the mapper—exactly where it belongs.
Logging Enhancements
Added logs at key points:
log.info("Starting user registration for username: {}", command.username());
RegisterUserResult result = service.register(command);
log.info("User registered successfully with ID: {}", result.userId());
Phase 5: Final Polish (PR #227)
Commit: 6ca363e — Controller line formatting
Minor readability tweak.
Commit: e86fab4 — BCryptPasswordEncoder variable rename
Renamed encoder variable for consistency:
Before
private final BCryptPasswordEncoder passwordEncoder;
After
private final BCryptPasswordEncoder bCryptPasswordEncoder;
PR #227 merged: Tue Jan 20 22:36:24 2026
Tiny change, but consistency compounds.
Architecture Evolution
Before Refactoring
Controller (DTO) → Service (DTO) → Entity returned
Problems:
Domain coupled to web DTOs
Entity exposed to the web layer
API changes risk cascading into domain changes
Harder to test domain logic cleanly
After Refactoring
Controller (RequestDTO)
→ AppUserMapper → RegisterUserCommand
→ Service → RegisterUserResult
→ AppUserMapper → ResponseDTO
Benefits:
✅ Domain independent of web concerns
✅ Clear boundary contracts (Commands/Results)
✅ Entity stays internal
✅ Mapper protects both sides
✅ API can evolve without rewiring domain logic
Files Modified Summary
UserRegistrationRequestCommand.java— deleted (replaced)RegisterUserCommand.java— createdRegisterUserResult.java— createdUserRegistrationMapper.java— renamed →AppUserMapperAppUserMapper.java— enhanced (+22 lines)RegisterUserRequestDTO.java— renamedRegisterUserResponseDTO.java— renamed + enhanced (added userId)UserRegistrationService.java— command in, result outUserRegistrationController.java— mapper-driven conversionsUserRegistrationControllerTest.java— stronger assertions
Total: 10 unique files touched, ~80 net lines added
Pull Request Timeline
| PR | Title | Merged |
| #223 | Initial Command Introduction | 18:52:17 |
| #224 | DTO Naming Consistency | 20:29:58 |
| #225 | Formatting & Test Enhancement | 22:03:50 |
| #226 | Mapper Method Addition | 22:21:27 |
| #227 | Final Variable Rename | 22:36:24 |
Time span: ~3h 44m (18:52 → 22:36)
Merge conflicts: 0 (blessed timeline)
Pattern Recognition (What This Refactor Demonstrates)
Command Pattern: encapsulate “do this” as a domain object
Result Object Pattern: return focused, stable data instead of entities
Anti-Corruption Layer: mapper prevents boundary bleed
Incremental Refactoring: small PRs, low risk, always mergeable
Naming Consistency: verb-first naming aligns code with intent
Reflection: DTOs Belong at Boundaries
This refactor was “small” in terms of code, but big in terms of clarity.
The key insight I’m taking forward:
DTOs should stay at the edges.
The domain should speak its own language—Commands, Results, Entities—not the language of HTTP.
Also: tiny records with focused data are absurdly powerful.RegisterUserCommand and RegisterUserResult are simple, but they turn implicit coupling into explicit contracts.
And finally: naming matters more than it seems.RegisterUserRequestDTO is not just prettier than UserRegistrationRequestDTO. It carries intent. It matches the rest of my command-oriented design. It makes the system feel coherent.
Next Steps
Immediate:
Apply the same command/result pattern to other user operations
Add validation annotations to command objects
Introduce error result objects for failure cases
Future:
Extract registration into a dedicated bounded context
Emit domain events on successful registration
Add audit logging and confirmation workflows
Integration tests for full registration flow
Metrics (Because We Measure What We Respect)
Pull Requests: 5 (all merged)
Commits: 13 total (9 functional + 4 merges)
Files Modified: 10
Net Lines Changed: ~80
Time Span: ~4 hours
Merge Conflicts: 0
That’s today’s devlog: a boundary leak patched, a domain language clarified, and a mapper promoted from “utility class” to “architectural bouncer.”
Onward.




