Skip to content

Lab 02 DefectsΒ #67

Description

@antonyboom

Lab 02 (Spring Boot / Java Track) β€” Defect Report

Lab: lab-02-requirements-to-code-java.md
Track affected: 🟩 Spring Boot / Java only (.NET, Kotlin and Swift versions are separate files)
Date: 2026-09-21
Status: Open β€” lab implemented successfully with workarounds, lab document not yet corrected
Related: LAB_01_SPRINGBOOT_DEFECTS.md Β· LAB_02_SPRINGBOOT_TODO.md


Summary

The lab was written against a different codebase layout than the one in src-springboot/.
Package names, folder paths, the Task entity shape and the API surface all differ. Four
defects are blocking: following the lab literally produces code that does not compile, and
two steps instruct participants to violate the repository's own architecture rules.

Backlog Items 1–3 were implemented end to end despite this. The reference implementation is
green (182 tests, BUILD SUCCESS), but only because of the workarounds below.

# Severity Defect Participant impact
1 πŸ”΄ Blocking Wrong package names and file paths throughout Nothing compiles; files land in the wrong modules
2 πŸ”΄ Blocking JPA annotations placed on the domain entity Violates Clean Architecture; omits all mapper work
3 πŸ”΄ Blocking Claims POST /api/tasks does not exist The "RED phase" never happens as described
4 πŸ”΄ Blocking Controller-local exception handler Silently overrides the global RFC 7807 handler
5 🟑 Moderate mvn spring-boot:run fails as documented App will not start; no database
6 🟑 Moderate No warning that existing tests break ~80 call sites across 5 test classes break at once
7 🟑 Moderate Hardcoded sample dates already in the past The "valid request" examples return 400
8 🟑 Moderate Troubleshooting advises the wrong repository pattern Would pull Spring Data into the domain module
9 🟒 Minor -Dtest= commands fail for unrelated reasons Confusing errors, wrong lesson
10 🟒 Minor Expected test counts do not match this repo Participants think they missed something
11 🟒 Minor Prerequisite "no uncommitted changes" is false Blocks a careful participant at step 0

Defect 1 β€” Wrong package names and file paths throughout

Severity: πŸ”΄ Blocking

Where

Roughly a dozen code blocks across Parts 2, 3 and 4.

What

Dimension Lab says Repo actually has
Root package com.taskmanager.* com.example.taskmanager.*
Domain path src-springboot/domain/entities/Task.java src-springboot/taskmanager-domain/src/main/java/com/example/taskmanager/domain/tasks/Task.java
Application path src-springboot/application/services/ src-springboot/taskmanager-application/src/main/java/com/example/taskmanager/application/services/
API layer name presentation api
Test path src-springboot/test/java/... src-springboot/<module>/src/test/java/...
Value objects domain/valueobjects/ No such package; TaskId/TaskStatus live in domain.tasks

Why it matters

src-springboot is a four-module Maven reactor (taskmanager-domain, -application,
-infrastructure, -api). The lab describes a flat single-module layout that does not exist.
Every path in the lab is wrong, and every package declaration in every snippet is wrong.

Proposed fix

Correct all packages to com.example.taskmanager.*, all paths to real module paths, and
rename "presentation" to "api" throughout. Put Priority in domain.tasks alongside
TaskId and TaskStatus rather than inventing a valueobjects package β€” the repository
instructions call for feature-oriented packaging.

Workaround applied

Used the real paths and packages. Priority created at
taskmanager-domain/src/main/java/com/example/taskmanager/domain/tasks/Priority.java.


Defect 2 β€” JPA annotations placed on the domain entity

Severity: πŸ”΄ Blocking

Where

  • Part 2.3 "Implement Task Entity (GREEN Phase)" β€” sample entity
  • Part 3 "Key Learning Points" claims "Domain Purity: No Spring/infrastructure concerns in entities"

What

The sample Task entity is annotated for persistence:

@Entity
@Table(name = "tasks")
public class Task {
    @Id private UUID id;
    @Column(nullable = false) private String title;
    @Enumerated(EnumType.STRING) private Priority priority;
    ...
}

The repository's domain Task is a pure POJO with zero JPA imports. Persistence is
handled by a separate TaskEntity plus a TaskMapper anti-corruption layer in the
infrastructure module.

Why it matters

Two problems, both serious.

First, it contradicts .github/instructions/springboot.instructions.md, which states
plainly: "Domain β†’ no deps (pure Java, no Spring)". The lab's own "Key Learning Points"
section then congratulates the participant on domain purity they just destroyed.

Second, and more practically, because the lab assumes an annotated domain entity it never
mentions the mapping layer at all
. A participant adding priority and dueDate by
following the lab will not touch:

  • TaskEntity β€” needs priority and due_date columns
  • TaskMapper.toEntity() / toDomain() β€” needs to map both new fields
  • TaskMapperTest β€” round-trip assertions need extending

The new fields would silently fail to persist.

Proposed fix

Rewrite Part 2.3 to add the fields to the pure-POJO Task, then add a distinct sub-step
covering the infrastructure round trip. Remove the JPA annotations from the sample entirely.

Workaround applied

Added priority/dueDate to the pure domain Task, then separately extended TaskEntity,
TaskMapper and TaskMapperTest.

Two subtleties the lab does not raise, both discovered during implementation:

  1. reconstitute() must not re-validate the due date. Creation rejects past due dates.
    Applying that rule on load makes any task whose deadline has passed permanently
    unloadable. The stored value is now assigned directly.
  2. reconstitute() was silently discarding createdAt. The pre-existing code accepted
    the parameter and then overwrote it with LocalDateTime.now(), with a comment admitting
    the limitation. Fixed while widening the signature, since the round-trip test now asserts
    on it.

Defect 3 β€” Claims POST /api/tasks does not exist

Severity: πŸ”΄ Blocking

Where

Part 3.2 Step 2, "Write Integration Tests FIRST (RED Phase)".

What

The lab states:

Run the integration tests - they should FAIL with 404 Not Found (endpoint doesn't exist yet)

Expected result: All integration tests fail. This is the RED phase! βœ…

TaskController already implements POST /api/tasks, and seven further endpoints:
GET /{id}, GET /, PATCH /{id}, PUT /{id}/start, PUT /{id}/complete,
PUT /{id}/cancel, DELETE /{id}.

Why it matters

The participant gets 201 Created, not 404. The lab's central TDD demonstration β€” watch it
fail, then make it pass β€” does not occur. Worse, a participant who trusts the lab may conclude
their test setup is broken and start debugging working code.

Proposed fix

Reframe Part 3.2 as extending an existing endpoint. The honest RED signal is an assertion
failure on the missing priority field in the response body, not a 404. That is still a
perfectly good TDD demonstration, and it is true.

Workaround applied

Extended the existing endpoint. CreateTaskRequest and TaskResponse were widened rather
than duplicated, preserving their existing @NotBlank / @Size validation and the
TaskResponse.from(Task) factory.


Defect 4 β€” Controller-local exception handler overrides the global one

Severity: πŸ”΄ Blocking

Where

Part 3.2 Step 3 β€” sample TaskController.

What

The sample controller declares its own handler and error shape:

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException ex) {
    return ResponseEntity.badRequest()
        .body(new ErrorResponse("Validation Error", ex.getMessage()));
}

record ErrorResponse(String error, String message) {}

The repository already maps IllegalArgumentException to a 400 RFC 7807 ProblemDetail in
GlobalExceptionHandler (@RestControllerAdvice), alongside TaskNotFoundException β†’ 404,
IllegalStateException β†’ 400, MethodArgumentNotValidException β†’ 400 with a field-keyed
errors map, and a catch-all β†’ 500.

Why it matters

A controller-local @ExceptionHandler takes precedence over @RestControllerAdvice.
Adding it silently changes the API's error contract from standards-compliant ProblemDetail
to a bespoke {error, message} shape β€” for this one controller only, producing an API that
is internally inconsistent. Existing tests asserting on $.title and $.errors.* would break.

Proposed fix

Delete the handler and the ErrorResponse record from the lab sample. Point participants at
GlobalExceptionHandler and explain that throwing the right exception from the domain is
sufficient β€” a genuinely better lesson than hand-rolling error responses in a controller.

Workaround applied

No local handler added. Priority.fromString(...) throws IllegalArgumentException, which
the existing global handler converts to a 400 ProblemDetail. Verified against a running
server:

priority "SUPER_URGENT" -> 400  Invalid priority: SUPER_URGENT. Valid values: [LOW, MEDIUM, HIGH, CRITICAL]
dueDate "2020-01-01"    -> 400  Task due date must be in the future
priority omitted        -> 400  errors.priority = Task priority is required

Defect 5 β€” mvn spring-boot:run fails as documented

Severity: 🟑 Moderate

Where

Part 4.1 "Run the API".

What

The lab gives:

cd src-springboot
mvn spring-boot:run

Two independent problems.

a) Wrong profile. The default application.yml targets PostgreSQL at
localhost:5432/taskmanager with ddl-auto: validate. Without a running Postgres the
context fails to start.

b) Wrong module, and stale artifacts. spring-boot:run at the reactor root has no single
application to run. Targeting -pl taskmanager-api resolves taskmanager-domain from
~/.m2, which may predate the participant's own changes:

java.lang.NoClassDefFoundError: com/example/taskmanager/domain/tasks/Priority

This surfaces as HTTP 500 on every request, with the app otherwise appearing healthy.

Proposed fix

Document the verified sequence:

cd src-springboot
mvn clean install -DskipTests
mvn spring-boot:run -pl taskmanager-api -Dspring-boot.run.profiles=dev

Windows PowerShell requires quoting: "-Dspring-boot.run.profiles=dev".

The dev profile uses in-memory H2 with ddl-auto: create-drop and exposes the H2 console at
/h2-console. Worth mentioning, since participants will want to inspect the new columns.

Workaround applied

Used the sequence above. Verified: the app starts and
POST /api/tasks {"title":"Smoke test","priority":"critical","dueDate":"2027-04-15T17:00:00"}
returns 201 with priority=CRITICAL.


Defect 6 β€” No warning that existing tests will break

Severity: 🟑 Moderate

Where

Part 2.2/2.3, where priority becomes a required constructor argument.

What

The lab treats Task.create(title, description, priority, dueDate) as though it were a new
method on a greenfield entity. In this repository, making priority required is a breaking
change
to Task.create(String) and Task.create(String, String), which are called from:

File Call sites
TaskTest 36
TaskServiceTest 27
JpaTaskRepositoryAdapterIntegrationTest 20
TaskMapperTest 8
TaskApiIntegrationTest 8

Around 80 call sites fail to compile the moment the signature changes, across four modules.

Why it matters

Participants hit a wall of compilation errors with no guidance, and no indication whether they
have made a mistake or this is expected. It is also a missed teaching opportunity: repairing
a test suite after a deliberate breaking change is exactly the kind of work TDD is supposed to
make safe.

Proposed fix

Add an explicit step: "Expect roughly 80 compilation errors across four modules. This is the
cost of making a field required β€” here is how to work through them." Then show the pattern.

Alternatively, offer the non-breaking variant (optional priority defaulting to MEDIUM) as a
documented fork in the road, with the tradeoff stated.

Workaround applied

Took the breaking change deliberately, then repaired all call sites. Full reactor green.


Defect 7 β€” Hardcoded sample dates are already in the past

Severity: 🟑 Moderate

Where

  • Part 3.2 Step 4 β€” tasks.http sample uses 2026-04-15T17:00:00
  • Part 4.3 β€” curl "Valid Request" uses 2026-04-25T17:00:00
  • Part 4.3 β€” expected response body repeats 2026-04-25T17:00:00

What

Both dates are in the past as of 2026-09-21. The lab's own validation rule rejects past due
dates, so the examples labelled "valid" now return 400 Bad Request.

Why it matters

Self-contradicting examples. A participant following Part 4.3 exactly gets a validation error
from the request the lab says should succeed, and no explanation.

Proposed fix

Stop hardcoding. Either instruct "use any date at least a day in the future", or use a
far-future constant that will not expire during the lab's useful life. Add a note that these
values need periodic review.

Workaround applied

The delivered src-springboot/tasks.http uses 2027 dates and carries a comment explaining the
constraint.


Defect 8 β€” Troubleshooting advises the wrong repository pattern

Severity: 🟑 Moderate

Where

Troubleshooting β†’ "Repository Pattern Not Working".

What

Solution: Ensure repository extends JpaRepository<Task, UUID> in domain layer

Why it matters

Exactly backwards for this repository, and for Clean Architecture generally. TaskRepository
is a domain port β€” a plain interface with business-intent methods. JpaTaskRepositoryAdapter
in the infrastructure module implements it, delegating to a package-private
SpringDataTaskRepository extends JpaRepository<TaskEntity, UUID>.

Following this advice would pull Spring Data into the domain module and invert the dependency
direction the lab teaches three sections earlier.

Proposed fix

Replace with guidance on adding a method to the port and implementing it in the adapter. This
matters for Backlog Items 4 and 5, which both need new query methods.

Workaround applied

None needed β€” no new repository methods were required for Items 1–3.


Defect 9 β€” -Dtest= commands fail for unrelated reasons

Severity: 🟒 Minor

Where

Parts 3.1 Step 3, 3.1 Step 4, 3.2 Step 2, 3.2 Step 3.

What

The lab gives mvn test -Dtest=TaskServiceImplTest. Three separate traps:

  1. Surefire aborts on the first module with no match:
    No tests matching pattern "TaskServiceImplTest" were executed!
  2. PowerShell splits unquoted -D properties at the dot:
    Unknown lifecycle phase ".failIfNoSpecifiedTests=false"
  3. -pl <module> alone resolves stale dependency jars from ~/.m2, causing
    NoClassDefFoundError at runtime β€” the same root cause as Defect 5b.

Proposed fix

# macOS / Linux
mvn test -Dtest=TaskServiceTest -Dsurefire.failIfNoSpecifiedTests=false
# Windows PowerShell β€” quotes required
mvn test "-Dtest=TaskServiceTest" "-Dsurefire.failIfNoSpecifiedTests=false"

Add a note: prefer the full reactor (mvn test), or -am, when a module's dependencies have
changed. Identical to Lab 01 Defect 5, plus the stale-jar trap.


Defect 10 β€” Expected test counts do not match this repo

Severity: 🟒 Minor

Where

Part 3.3 "Run Full Test Suite".

What

Unit tests: All passing (10+ for TaskServiceImpl, 11+ for Task entity)
Integration tests: All passing (6+ for TaskControllerIntegrationTest)

Actual totals after completing Items 1–3: 65 domain, 69 application, 27 infrastructure,
21 API β€” 182.

Proposed fix

Express as deltas ("add at least N tests covering…") rather than absolute totals, which drift
every time the reference implementation changes.


Defect 11 β€” Prerequisite "no uncommitted changes" is false

Severity: 🟒 Minor

Where

Prerequisites: "Repository at clean state: git status shows no uncommitted changes".

What

Lab 01's NotificationService output is uncommitted when Lab 02 begins, and Lab 01 never
instructs participants to commit.

Proposed fix

Either add a commit step at the end of Lab 01, or soften this to "commit or stash your Lab 01
work before starting".


Verification status of the reference implementation

cd src-springboot && mvn clean test β†’ BUILD SUCCESS

Module Tests Before After
taskmanager-domain 65 36 +29
taskmanager-application 69 66 +3
taskmanager-infrastructure 27 25 +2
taskmanager-api 21 8 +13
Total 182 135 +47

Backlog Items 1–3 complete; Items 4–5 not started. See
task-priorities-backlog.md.

Deliberate deviations

Not defects, but flagged so reviewers do not read them as mistakes:

  1. Concrete TaskService, no interface. The lab introduces TaskService +
    TaskServiceImpl. The repo has a single concrete class, consistent with the instruction to
    avoid unnecessary abstractions. A one-implementation interface adds a file and no value here.
  2. Priority in domain.tasks, not domain.valueobjects. Matches the existing placement
    of TaskId and TaskStatus, and the feature-oriented packaging rule.
  3. Explicit int value on Priority. Follows the lab's own "recommended" variant. Worth
    noting it duplicates ordinal(); the justification is that an explicit value is stable
    under reordering, whereas ordinal() is not.

Known gaps carried forward

  1. No schema migration. Production runs ddl-auto: validate; dev and test run
    create-drop, which hides the missing priority and due_date columns. A migration is
    required before this reaches a real database. The lab never mentions migrations.
  2. No injected Clock. Due-date validation reads LocalDateTime.now() directly, so
    boundary conditions are untestable. The lab mentions Clock only in a troubleshooting
    footnote. Backlog Item 5 ("due soon") will make this painful.
  3. Two different 400 body shapes. Bean-validation failures return a field-keyed errors
    map; domain failures return a plain detail string. Both are valid RFC 7807, but clients
    must handle both. Worth a deliberate decision rather than an accident.

Suggested triage

Before the next workshop run β€” Defects 1–4. Participants cannot complete the Spring Boot
track by following the document as written.

Soon after β€” Defects 5–8. These produce confusing failures and teach at least one pattern
that actively contradicts the repository's architecture.

When convenient β€” Defects 9–11. Command corrections and copy edits.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions