β Create User Flow Cheatsheet¶
(Spring Boot β Clean layering, upgrade-ready)
π Folder Structure¶
users/
web/
UserController.java
CreateUserRequest.java (added only when needed)
UserResponse.java
app/
CreateUserInput.java
UserService.java
domain/
User.java
UserRepository.java
- Always:
CreateUserInput,UserResponse - Only add when tripwire hits:
CreateUserRequest
π― CreateUserInput (the core input)¶
Transport-agnostic, validated, normalized.
// app/CreateUserInput.java
public record CreateUserInput(
@NotBlank String name,
@Email @NotBlank String email
) {
public CreateUserInput {
if (name != null) name = name.trim();
if (email != null) email = email.trim().toLowerCase();
}
}
Why: clean, pure, reusable for REST, Kafka, CLI, etc.
π Web Controller (initial simple phase)¶
Directly bind
CreateUserInputuntil tripwire.
// web/UserController.java
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService service;
private final UserMapper mapper;
@PostMapping
public ResponseEntity<UserResponse> create(@Valid @RequestBody CreateUserInput in) {
var created = service.create(in);
var location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(mapper.toResponse(created));
}
}
π¨βπΌ Service¶
// app/UserService.java
@Service
public class UserService {
private final UserRepository users;
private final UserMapper mapper;
@Transactional
public User create(CreateUserInput in) {
if (users.existsByEmailIgnoreCase(in.email())) {
throw new ConflictException("Email already taken");
}
var u = mapper.toEntity(in);
return users.save(u);
}
}
π§ Entity¶
// domain/User.java
@Entity
public class User {
@Id @GeneratedValue
private UUID id;
private String name;
private String email;
@Version
private long version;
}
π Mapping (MapStruct)¶
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "id", ignore = true)
@Mapping(target = "version", ignore = true)
User toEntity(CreateUserInput in);
UserResponse toResponse(User user);
}
π€ Response DTO¶
// web/UserResponse.java
public record UserResponse(
UUID id,
String name,
String email,
long version
) {}
π§ Tripwire Upgrade Section¶
(When REST needs JSON quirks OR 2nd input channel arrives)
Add Request DTO:
// web/CreateUserRequest.java
public record CreateUserRequest(
@JsonProperty("user_name") String name,
@Email @NotBlank String email
) {}
Update controller to map:
@PostMapping
public ResponseEntity<UserResponse> create(@Valid @RequestBody CreateUserRequest req) {
var in = new CreateUserInput(req.name(), req.email());
var created = service.create(in);
return ResponseEntity.ok(mapper.toResponse(created));
}
Service stays untouched β the win.
π§ Mental Reminders Cheat-Box¶
Before tripwires β Input in controller β No DTO β Cleanest flow
After tripwire
π Request β Input in controller
π§Ό Input stays pure
π« Never put Jackson into Input
π Service signature does not change
Tripwires
- JSON rename (
@JsonProperty) - Custom date format
- Multipart
- OpenAPI field docs
- Second input channel (Kafka/CLI)
- Public API stability needed
π TL;DR Card¶
| Layer | Type | When |
|---|---|---|
| web | CreateUserRequest |
only after JSON quirks / multi-transport |
| app | CreateUserInput |
always |
| domain | User |
always |
| web | UserResponse |
always |
Mapping directions:
Never reverse.