Skip to content

Instantly share code, notes, and snippets.

@fResult
Last active May 16, 2025 17:39
Show Gist options
  • Select an option

  • Save fResult/77c4341a49f8135ea8762b84b3d7fffe to your computer and use it in GitHub Desktop.

Select an option

Save fResult/77c4341a49f8135ea8762b84b3d7fffe to your computer and use it in GitHub Desktop.
import static java.time.temporal.ChronoUnit.HOURS;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.function.Supplier;
class TryTypeLevelStateTransition {
private static final SpringTaskRepository repo = new SpringTaskRepository();
private static final TaskRepositoryAdapter repoAdapter = new TaskRepositoryAdapter(repo);
private static final TaskManagement management = new TaskManagement();
private static final TaskUseCase useCase = new TaskUseCase(repoAdapter, management);
private static final TaskController controller = new TaskController(useCase);
public static void main(String[] args) {
// Step 1: Create a new dinner plan task
final var bodyToCreate = new TaskCreationRequest(
/*title*/ "Have dinner",
/*description*/ null,
/*status*/ "TODO");
invokeNewTaskApi(bodyToCreate);
// Step 2: Check all my current tasks
invokeFindTasksAPI();
// Step 3: Add important details to our dinner plan
final var bodyToPartialUpdate = new TaskUpdateRequest(
/*id*/ null,
/*title*/ null,
/*description*/ "[NOTE!] Need to ask Trinity where we will eat for dinner",
/*status*/ null,
/*completedAt*/ null);
invokeUpdateTaskAPI(1, bodyToPartialUpdate);
// Step 4: Look up our updated dinner task
invokeFindTaskByIdAPI(1);
// Step 5: Move our dinner task to the "doing" state (we're planning it now)
invokeAdvanceTaskAPI(1);
// Error scenario: Try to advance a non-existent task
invokeAdvanceTaskAPI(9999L);
// Task completion flow: Complete a task and verify we can't advance it further
try {
// First complete the task `/api/tasks/:id/complete
final var markedAsDoneTask = controller.markAsDone(111L);
System.out.println("[HTTP 200] Task after mark as DONE: " + markedAsDoneTask);
// Then try to advance it further (which should do nothing per domain rules)
final var advancedTask = controller.advanceTask(111L);
System.out.println("[HTTP 200] Task after attempting to advance beyond DONE: " + advancedTask.status());
} catch (NotFoundException e) {
// Simulates @ExceptionHandler(NotFoundException.class) in @ControllerAdvice
System.out.println("HTTP 404: " + e.getMessage());
} catch (IllegalStateException e) {
// Simulates @ExceptionHandler(IllegalStateException.class) for domain exceptions in
// @ControllerAdvice
System.out.println("HTTP 422: " + e.getMessage());
} catch (Exception e) {
// Simulates generic exception handler in @ControllerAdvice
System.out.println("HTTP 500: Internal Server Error - " + e.getMessage());
}
}
private static void invokeFindTasksAPI() {
try {
// Invoke GET `/api/tasks`
final var tasks = controller.all();
System.out.println("[HTTP 200] Successfully retrieved all tasks: " + tasks);
} catch (Exception e) {
// Simulates generic exception handler in @ControllerAdvice
System.out.println("[HTTP 500]: Internal Server Error - " + e.getMessage());
}
}
private static void invokeFindTaskByIdAPI(long id) {
try {
// Invoke GET `/api/tasks/:id`
final var task = controller.byId(id);
System.out.println("[HTTP 200] Successfully retrieved task: " + task);
} catch (NotFoundException e) {
// Simulates @ExceptionHandler(NotFoundException.class) in @ControllerAdvice
System.out.println("[HTTP 404]: " + e.getMessage());
} catch (Exception e) {
// Simulates generic exception handler in @ControllerAdvice
System.out.println("[HTTP 500]: Internal Server Error - " + e.getMessage());
}
}
private static void invokeNewTaskApi(TaskCreationRequest body) {
try {
// Invoke POST `/api/tasks`
final var createdTask = controller.create(body);
System.out.println("[HTTP 201] Successfully created task: " + createdTask);
} catch (IllegalArgumentException e) {
// Simulates @ExceptionHandler(IllegalArgumentException.class) in @ControllerAdvice
System.out.println("[HTTP 400]: " + e.getMessage());
} catch (Exception e) {
// Simulates generic exception handler in @ControllerAdvice
System.out.println("[HTTP 500]: Internal Server Error - " + e.getMessage());
}
}
private static void invokeUpdateTaskAPI(long id, TaskUpdateRequest body) {
try {
// Invoke PATCH `/api/tasks/:id`
final var updatedTask = controller.update(id, body);
System.out.println("[HTTP 200] Successfully updated task: " + updatedTask);
} catch (NotFoundException e) {
// Simulates @ExceptionHandler(NotFoundException.class) in @ControllerAdvice
System.out.println("[HTTP 404]: " + e.getMessage());
} catch (Exception e) {
// Simulates generic exception handler in @ControllerAdvice
System.out.println("[HTTP 500]: Internal Server Error - " + e.getMessage());
}
}
private static void invokeAdvanceTaskAPI(long id) {
try {
// Invoke PATCH `/api/tasks/:id/advance
final var advancedTask = controller.advanceTask(id);
System.out.println("[HTTP 200] Successfully advanced task: " + advancedTask);
} catch (NotFoundException e) {
// Simulates @ExceptionHandler(NotFoundException.class) in @ControllerAdvice
System.out.println("[HTTP 404]: " + e.getMessage());
} catch (Exception e) {
// Simulates generic exception handler in @ControllerAdvice
System.out.println("[HTTP 500]: Internal Server Error - " + e.getMessage());
}
}
}
// @RestController
// RequestMapping("/api/v1/tasks")
class TaskController {
private final TaskUseCase taskUseCase;
TaskController(TaskUseCase taskUse) {
this.taskUseCase = taskUse;
}
// @GetMapping
public List<TaskResponse> all() {
return taskUseCase.all();
}
// @GetMapping("/{id}")
public TaskResponse byId(/*@PathVariable*/ long id) {
return taskUseCase.byId(id);
}
// @PostMapping
public TaskResponse create(/*@RequestBody*/ TaskCreationRequest body) {
return taskUseCase.create(body); // Should return HTTP 201 if success
}
// @PatchMapping("/{id}")
public TaskResponse update(/*@PathVariable*/ long id, /*@RequestBody*/ TaskUpdateRequest body) {
return taskUseCase.update(id, body);
}
// @PatchMapping("/{id}/advance")
public TaskResponse advanceTask(/*@PathVariable*/ long id) {
return taskUseCase.advanceTask(id);
}
// @PatchMapping("/{id}/revert")
public TaskResponse revertTask(/*@PathVariable*/ long id) {
return taskUseCase.revertTask(id);
}
// @PatchMapping("/{id}/complete")
public TaskResponse markAsDone(/*@PathVariable*/ long id) {
return taskUseCase.markAsDone(id);
}
// @PatchMapping("/{id}/reset")
public TaskResponse resetToTodo(/*@PathVariable*/ long id) {
return taskUseCase.resetToTodo(id);
}
// @PatchMapping("/{id}/progress")
public TaskResponse progress(/*@PathVariable*/ long id) {
return taskUseCase.progress(id);
}
}
class TaskUseCase {
private final TaskRepository taskRepository;
private final TaskManagement taskManagement;
TaskUseCase(TaskRepository taskRepository, TaskManagement taskManagement) {
this.taskRepository = taskRepository;
this.taskManagement = taskManagement;
}
public List<TaskResponse> all() {
return taskRepository.findAll().stream().map(TaskResponse::fromDomain).toList();
}
public TaskResponse byId(Long id) {
return taskRepository.findById(id)
.map(TaskResponse::fromDomain)
.orElseThrow(throwNotFound(id, Task.class));
}
public TaskResponse create(TaskCreationRequest body) {
return Optional.of(body)
.map(Task::fromRequest)
.map(taskRepository::save)
.map(TaskResponse::fromDomain)
.orElseThrow();
}
public TaskResponse update(Long id, TaskUpdateRequest body) {
return taskRepository.findById(id)
.map(this.toTaskUpdate(body).andThen(taskRepository::save).andThen(TaskResponse::fromDomain))
.orElseThrow(throwNotFound(id, Task.class));
}
public TaskResponse advanceTask(Long id) {
return executeTaskOperation(id, taskManagement::advanceTask);
}
public TaskResponse revertTask(Long id) {
return executeTaskOperation(id, taskManagement::revertTask);
}
public TaskResponse markAsDone(Long id) {
return executeTaskOperation(id, taskManagement::markAsDone);
}
public TaskResponse resetToTodo(Long id) {
return executeTaskOperation(id, taskManagement::resetToTodo);
}
public TaskResponse progress(Long id) {
return executeTaskOperation(id, taskManagement::progress);
}
private TaskResponse executeTaskOperation(Long id, Function<Task, Task> operation) {
return taskRepository.findById(id)
.map(operation.andThen(taskRepository::save).andThen(TaskResponse::fromDomain))
.orElseThrow(throwNotFound(id, Task.class));
}
private Function<Task, Task> toTaskUpdate(TaskUpdateRequest body) {
return existingTask -> switch (existingTask) {
case Todo todo -> new Todo(
toUpdatedValue(todo.id(), body.id()),
toUpdatedValue(todo.title(), body.title()),
toUpdatedValue(todo.description(), body.description()));
case Doing doing -> new Doing(
toUpdatedValue(doing.id(), body.id()),
toUpdatedValue(doing.title(), body.title()),
toUpdatedValue(doing.description(), body.description()));
case Done done -> new Done(
toUpdatedValue(done.id(), body.id()),
toUpdatedValue(done.title(), body.title()),
toUpdatedValue(done.description(), body.description()),
toUpdatedValue(done.completedAt(), body.completedAt()));
};
}
private <T> T toUpdatedValue(T existing, T replacer) {
return Optional.ofNullable(replacer).orElse(existing);
}
private Supplier<NotFoundException> throwNotFound(Long resourceId, Class<?> resourceName) {
return NotFoundException.toThrow(
String.format("Not found %s for ID %d", resourceName.getSimpleName(), resourceId));
}
}
/* ==================================
* ========== Domain Layer ==========
* ================================== */
// ========== Domain Models ==========
sealed interface Task permits Todo, Doing, Done {
Long id();
String title();
String description();
String displayStatus();
static Task fromRequest(TaskCreationRequest body) {
return switch (TaskState.of(body.status())) {
case TODO -> new Todo(null, body.title(), body.description());
case DOING -> new Doing(null, body.title(), body.description());
case DONE -> new Done(null, body.title(), body.description(), Instant.now());
};
}
static Task fromRequest(TaskUpdateRequest body) {
return switch (TaskState.of(body.status())) {
case TODO -> new Todo(body.id(), body.title(), body.description());
case DOING -> new Doing(body.id(), body.title(), body.description());
case DONE -> new Done(body.id(), body.title(), body.description(), Instant.now());
};
}
}
record Todo(Long id, String title, String description) implements Task {
public Todo(String title, String description) {
this(null, title, description);
}
@Override
public String displayStatus() {
return "TODO: " + title;
}
}
record Doing(Long id, String title, String description) implements Task {
public Doing(String title, String description) {
this(null, title, description);
}
@Override
public String displayStatus() {
return "IN PROGRESS: " + title;
}
}
record Done(Long id, String title, String description, Instant completedAt) implements Task {
public Done(String title, String description) {
this(null, title, description, Instant.now());
}
public Done(Long id, String title, String description) {
this(id, title, description, Instant.now());
}
public Done(String title, String description, Instant completedAt) {
this(null, title, description, completedAt);
}
@Override
public String displayStatus() {
return "DONE: " + title;
}
}
enum TaskState {
TODO,
DOING,
DONE;
public static TaskState of(String state) throws IllegalStateException {
final var states = Map.of("TODO", TODO, "DOING", DOING, "DONE", DONE);
if (!states.containsKey(state)) throw new IllegalStateException("State '" + state + "' is not in TaskState");
return states.get(state);
}
}
// ========== Domain Service ==========
class TaskManagement {
public Task advanceTask(Task task) {
return switch (task) {
case Todo(Long id, String title, String description) -> new Doing(id, title, description);
case Doing(Long id, String title, String description) -> new Done(id, title, description);
case Done ignored -> task;
};
}
public Task revertTask(Task task) {
return switch (task) {
case Done(Long id, String title, String description, Instant ignored) -> new Doing(id, title, description);
case Doing(Long id, String title, String description) -> new Todo(id, title, description);
case Todo ignored -> task;
};
}
public Task markAsDone(Task task) {
if (task instanceof Done) return task;
return new Done(task.id(), task.title(), task.description());
}
public Task resetToTodo(Task task) {
if (task instanceof Todo) return task;
return new Todo(task.id(), task.title(), task.description());
}
public Task progress(Task task) {
if (task instanceof Doing) return task;
return new Doing(task.id(), task.title(), task.description());
}
}
/* =======================================
* ========== Persistence Layer ==========
* ======================================= */
interface TaskRepository {
List<Task> findAll();
Optional<Task> findById(Long id);
Task save(Task entity);
}
// @Repository
class TaskRepositoryAdapter implements TaskRepository {
private final SpringTaskRepository springRepository;
public TaskRepositoryAdapter(SpringTaskRepository springRepository) {
this.springRepository = springRepository;
}
@Override
public List<Task> findAll() {
return springRepository.findAll().stream().map(this::mapToDomain).toList();
}
@Override
public Optional<Task> findById(Long id) {
return springRepository.findById(id).map(this::mapToDomain);
}
@Override
public Task save(Task domain) {
return Optional.ofNullable(domain)
.map(this::mapToEntity)
.map(springRepository::save)
.map(this::mapToDomain)
.orElseThrow();
}
private Task mapToDomain(TaskEntity entity) throws IllegalStateException {
return switch (TaskState.valueOf(entity.state())) {
case TODO -> new Todo(entity.id(), entity.title(), entity.description());
case DOING -> new Doing(entity.id(), entity.title(), entity.description());
case DONE -> new Done(entity.id(), entity.title(), entity.description());
};
}
private TaskEntity mapToEntity(Task domain) {
return switch (domain) {
case Todo(Long id, String title, String description) ->
new TaskEntity(id, title, description, TaskState.TODO.name(), null);
case Doing(Long id, String title, String description) ->
new TaskEntity(id, title, description, TaskState.DOING.name(), null);
case Done(Long id, String title, String description, Instant completedAt)
when completedAt == null ->
new TaskEntity(id, title, description, TaskState.DONE.name(), Instant.now());
case Done(Long id, String title, String description, Instant completedAt) ->
new TaskEntity(id, title, description, TaskState.DONE.name(), completedAt);
};
}
}
// interface SpringTaskRepository extends ListCrudRepository<Long, TaskEntity> {}
class SpringTaskRepository {
private static final AtomicLong ID_GENERATOR = new AtomicLong(0);
private static final Map<Long, TaskEntity> tasks = new HashMap<>() {{
put(111L, new TaskEntity(111L, "Have Lunch", null, TaskState.TODO.name(), null));
put(555L, new TaskEntity(555L, "Read/Reply Emails", null, TaskState.DOING.name(), null));
put(999L, new TaskEntity(999L, "Attend Monthly Meeting", null, TaskState.DONE.name(), Instant.now().minus(2, HOURS)));
}};
public List<TaskEntity> findAll() {
return tasks.values().stream().toList();
}
public Optional<TaskEntity> findById(Long id) {
return Optional.ofNullable(tasks.get(id));
}
public TaskEntity save(TaskEntity entity) {
Optional.ofNullable(entity.id())
.ifPresentOrElse(
id -> tasks.put(id, entity),
() -> {
final var nextID = ID_GENERATOR.addAndGet(1);
final var completedAt = entity.state().equals("DONE") ? Instant.now() : null;
final var entityToCreate = new TaskEntity(
nextID,
entity.title(),
entity.description(),
entity.state(),
completedAt);
tasks.put(nextID, entityToCreate);
});
return entity;
}
}
record TaskEntity(Long id, String title, String description, String state, Instant completedAt) {
public static TaskEntity fromTaskDomain(Task domain) {
return switch (domain) {
case Todo(Long id, String title, String description) ->
new TaskEntity(id, title, description, "TODO", null);
case Doing(Long id, String title, String description) ->
new TaskEntity(id, title, description, "DOING", null);
case Done(Long id, String title, String description, Instant completedAt)
when completedAt == null ->
new TaskEntity(id, title, description, "DONE", Instant.now());
case Done(Long id, String title, String description, Instant completedAt) ->
new TaskEntity(id, title, description, "DONE", completedAt);
};
}
}
/* ==================================================
* ========== DTOs (Data Transfer Objects) ==========
* ================================================== */
record TaskCreationRequest(String title, String description, String status) {}
record TaskUpdateRequest(
Long id, String title, String description, String status, Instant completedAt) {}
record TaskResponse(Long id, String title, String description, String status, Instant completedAt) {
public static TaskResponse fromDomain(Task domain) {
return switch (domain) {
case Todo(Long id, String title, String description) ->
new TaskResponse(id, title, description, TaskState.TODO.name(), null);
case Doing(Long id, String title, String description) ->
new TaskResponse(id, title, description, TaskState.DOING.name(), null);
case Done(Long id, String title, String description, Instant completedAt) ->
new TaskResponse(id, title, description, TaskState.DONE.name(), completedAt);
};
}
}
/* ================================
* ========== Exceptions ==========
* ================================ */
class NotFoundException extends RuntimeException {
public NotFoundException() {
super("Not found!");
}
public NotFoundException(String message) {
super(message);
}
public static Supplier<NotFoundException> toThrow(String message) {
return () -> new NotFoundException(message);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment