Tasks are organized by difficulty. Pick 1-2 tasks matching the candidate's level. Let them explore the codebase for ~10 minutes before starting.
Context: The TodoController is missing GET /api/todos/{id}. The UserController has a working example.
What to do:
- Add
getTodoById()toTodoService - Add
GET /api/todos/{id}toTodoController - Handle the case where the todo doesn't exist
Acceptance Criteria:
GET /api/todos/1returns the todo with status200GET /api/todos/999returns an appropriate error
Hints:
- Look at
UserService.getUserById()for reference - Use
orElseThrow()on the Optional returned byfindById()
Context: The TodoController is missing PUT /api/todos/{id}. The UserController has a working example.
What to do:
- Add
updateTodo()toTodoService - Add
PUT /api/todos/{id}toTodoController
Acceptance Criteria:
PUT /api/todos/1with a valid body updates and returns the todo- Only
title,description,priority, anddueDateshould be updatable PUT /api/todos/999returns an appropriate error
Hints:
- Look at
UserService.updateUser()for the pattern - Don't allow updating
completedhere — that's a separate concern (Task 5)
Context: The POST /api/todos endpoint accepts any input — even empty bodies. There's no validation.
What to do:
- Add validation annotations to
Todoentity (e.g.,@NotBlank,@NotNull) - Add
@Validto the controller method parameter - Add the
spring-boot-starter-validationdependency if needed
Acceptance Criteria:
titleis required and cannot be blankpriorityis requiredPOSTwith missing required fields returns400with meaningful error messages
Hints:
- Look into
jakarta.validation.constraintsannotations - You may need to add
spring-boot-starter-validationtopom.xml
Context: Try DELETE /api/users/1 — it returns a 500 error because the user has associated todos.
What to do:
- Investigate why the delete fails (foreign key constraint)
- Fix it so deleting a user also handles their todos
Acceptance Criteria:
DELETE /api/users/1succeeds without a500error- The user's todos are handled appropriately (cascaded delete or reassignment)
Hints:
- Look at the
@OneToManyannotation onUser.todos - Consider
cascadeandorphanRemovaloptions - Think about what should happen to a user's todos when they're deleted
Context: There's no way to mark a todo as completed through the API. The completed field exists but can't be toggled.
What to do:
- Add a
PATCH /api/todos/{id}/completeendpoint - It should toggle the
completedfield totrue
Acceptance Criteria:
PATCH /api/todos/1/completesetscompletedtotrueand returns the updated todo- Calling it on an already completed todo is idempotent (still returns
200) PATCH /api/todos/999/completereturns an appropriate error
Hints:
- Use
@PatchMappingin the controller - This is a good use case for a dedicated service method
Context: There are no unit tests for the service layer. Only a basic contextLoads test exists.
What to do:
- Write unit tests for
TodoService - Mock the
TodoRepositoryusing@MockBeanor Mockito - Test at least:
getAllTodos(),createTodo(),deleteTodo()
Acceptance Criteria:
- Tests run and pass with
./mvnw test - Repository is mocked (no database calls)
- At least 3 test methods covering the existing service methods
Hints:
- Use
@ExtendWith(MockitoExtension.class)and@Mock/@InjectMocks - Use
when(...).thenReturn(...)to stub repository methods - Use
verify(...)to check interactions
Context: GET /api/todos returns all todos with no way to filter or search.
What to do:
- Add query parameters to filter todos by
completed,priority, and/oruserId - Add a search parameter for
title(contains, case-insensitive)
Acceptance Criteria:
GET /api/todos?completed=falsereturns only incomplete todosGET /api/todos?priority=HIGHreturns only high-priority todosGET /api/todos?search=groceriesreturns todos with "groceries" in the title- Filters can be combined
Hints:
- Consider using Spring Data JPA query methods or
@Queryannotations - Alternatively, look into
JpaSpecificationExecutorfor dynamic queries - Keep the controller clean — filtering logic belongs in the service/repository
Context: GET /api/todos returns all 12 todos in a single response. This won't scale.
What to do:
- Add pagination support to
GET /api/todos - Accept
pageandsizequery parameters
Acceptance Criteria:
GET /api/todos?page=0&size=5returns the first 5 todos- Response includes pagination metadata (total elements, total pages, current page)
- Default behavior (no params) still works
Hints:
- Spring Data JPA has built-in pagination via
PageableandPage<T> - Look at
PagingAndSortingRepositoryor passPageabletofindAll() - Consider returning
Page<Todo>instead ofList<Todo>
Context: Errors return inconsistent responses. Some return stack traces, some return Spring's default error format.
What to do:
- Create a
@ControllerAdviceclass for global exception handling - Handle common exceptions:
RuntimeException,MethodArgumentNotValidException, etc. - Return a consistent error response format
Acceptance Criteria:
- All errors return a consistent JSON structure (e.g.,
{ "status": 404, "message": "...", "timestamp": "..." }) GET /api/todos/999returns a clean404instead of a500with stack trace- Validation errors return
400with field-level details
Hints:
- Use
@RestControllerAdviceand@ExceptionHandler - Create a custom
ErrorResponseclass - Look at how
UserServicethrowsRuntimeException— consider a custom exception
Context: CommentController injects repositories directly. There's no service layer, unlike User and Todo.
What to do:
- Create
CommentServicewith the business logic currently in the controller - Refactor
CommentControllerto useCommentService - Keep the same API behavior
Acceptance Criteria:
- All existing comment endpoints work exactly as before
- Controller only depends on
CommentService(not repositories) - Business logic (finding todo, setting timestamp) lives in the service
Hints:
- Follow the pattern in
UserServiceandTodoService - Move the
todoRepositorydependency to the service - The controller should be thin — just HTTP concerns
Context: The priority field on Todo is a plain String. There's no validation — you could set it to "BANANA" and it would save.
What to do:
- Create a
Priorityenum (e.g.,LOW,MEDIUM,HIGH) - Update
Todoto use the enum instead ofString - Handle invalid priority values gracefully
Acceptance Criteria:
POST /api/todoswith"priority": "HIGH"worksPOST /api/todoswith"priority": "BANANA"returns a400error- Existing seed data still loads correctly
Hints:
- Use
@Enumerated(EnumType.STRING)on the field - Jackson automatically deserializes strings to enums
- Consider what error message the user sees for invalid values
Context: There are no integration tests. The only test is contextLoads().
What to do:
- Write integration tests for
TodoControllerusing@SpringBootTestandMockMvc - Test the full request/response cycle against the H2 database
- Cover at least:
GET /api/todos,POST /api/todos,DELETE /api/todos/{id}
Acceptance Criteria:
- Tests run with
./mvnw test - Tests use the real Spring context and H2 database
- At least 3 integration tests covering different endpoints
- Tests verify HTTP status codes and response bodies
Hints:
- Use
@SpringBootTestwith@AutoConfigureMockMvc - Use
MockMvcto perform requests and assert responses - The H2 database is already configured — seed data will be available
Context: Call GET /api/todos and observe the SQL logs. You'll see multiple queries being fired — one for the todo list, then additional queries for each todo's user.
What to do:
- Identify the N+1 query issue in
TodoService.getAllTodos() - Fix it using a JPA-optimized approach
- Verify the fix by checking SQL logs
Acceptance Criteria:
GET /api/todosfires at most 2 queries (one for todos, one for users) instead of N+1- The response data is unchanged
- The fix is done at the JPA/repository level (not by restructuring the service)
Hints:
- Look at
@EntityGraphorJOIN FETCHqueries - The N+1 happens because lazy-loaded relationships are accessed in a loop
- Compare SQL output before and after your fix
Context: Todos have a dueDate field, but there's no way to find upcoming or overdue todos.
What to do:
- Add
GET /api/todos/overdue— returns todos past their due date that aren't completed - Add
GET /api/todos/upcoming?days=3— returns todos due within the next N days - Consider how this could be extended to a scheduled notification system
Acceptance Criteria:
GET /api/todos/overduereturns incomplete todos withdueDatebefore todayGET /api/todos/upcoming?days=7returns todos due within the next 7 days- Default
daysparameter is 3 if not specified - Completed todos are excluded from both endpoints
Hints:
- Use Spring Data JPA query methods with
LocalDate.now() @Querywith JPQL or native SQL may be needed for date comparisons- For the discussion part: talk about
@Scheduled, event-driven architecture, or message queues