# Devlog: User Registration Domain Refactoring

**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**

* `UserRegistrationRequestCommand` record in the domain layer
    
* Mapper methods in `UserRegistrationMapper`
    

**Modified**

* `UserRegistrationService` now accepts a command instead of a DTO
    
* `UserRegistrationController` now 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.

```java
public class UserRegistrationService {
    public UserEntity register(UserRegistrationRequestDTO dto) {
        // Service directly depends on web DTO
    }
}
```

**After:** service depends on a domain command.

```java
public class UserRegistrationService {
    public UserEntity register(UserRegistrationRequestCommand command) {
        // Service works with domain command
    }
}
```

And the controller became responsible for translating boundary → domain:

```java
@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` → `RegisterUserRequestDTO`
    
* `UserRegistrationResponseDTO` → `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` (replacing `UserRegistrationRequestCommand`)
    
* `RegisterUserResult` record to encapsulate service response
    

**Renamed**

* `UserRegistrationMapper` → `AppUserMapper`
    

**Modified**

* Service now returns `RegisterUserResult`
    
* Response 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**

```plaintext
Web Layer   Domain Layer       Domain Layer   Web Layer
   DTO   →    Command   →  Service  → Result  →  DTO
```

### Before vs After

**Before:** Service returned the entity directly.

```java
UserEntity result = service.register(command);
RegisterUserResponseDTO response = mapper.toResponseDTO(result);
```

**After:** Service returns a result object.

```java
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:

```java
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.”

```java
@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 in `AppUserMapper`
    

**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:

```java
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**

```java
private final BCryptPasswordEncoder passwordEncoder;
```

**After**

```java
private final BCryptPasswordEncoder bCryptPasswordEncoder;
```

**PR #227 merged:** Tue Jan 20 22:36:24 2026

Tiny change, but consistency compounds.

---

## Architecture Evolution

### Before Refactoring

```plaintext
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

```plaintext
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`](http://UserRegistrationRequestCommand.java) — deleted (replaced)
    
* [`RegisterUserCommand.java`](http://RegisterUserCommand.java) — created
    
* [`RegisterUserResult.java`](http://RegisterUserResult.java) — created
    
* [`UserRegistrationMapper.java`](http://UserRegistrationMapper.java) — renamed → `AppUserMapper`
    
* [`AppUserMapper.java`](http://AppUserMapper.java) — enhanced (+22 lines)
    
* [`RegisterUserRequestDTO.java`](http://RegisterUserRequestDTO.java) — renamed
    
* [`RegisterUserResponseDTO.java`](http://RegisterUserResponseDTO.java) — renamed + enhanced (added userId)
    
* [`UserRegistrationService.java`](http://UserRegistrationService.java) — command in, result out
    
* [`UserRegistrationController.java`](http://UserRegistrationController.java) — mapper-driven conversions
    
* [`UserRegistrationControllerTest.java`](http://UserRegistrationControllerTest.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.
