Assignment Service
This page covers lumie-backend/modules/assignment.
Responsibility
The assignment module owns:
- tenant-scoped assignments with explicit class and student targets;
- staff authoring workflows under
/v1/assignments; - student dashboard reads and self-submission APIs under
/v1/assignments/me; - exam-backed manual-answer submission storage for active assignments;
- the assignment deadline, active/closed state, and correct-answer visibility toggle;
- the internal lookup API used by other backend modules.
Source Paths
| Path | Role |
|---|---|
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/adapter/in/web/AssignmentController.java | Staff and student assignment HTTP routes |
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/adapter/in/web/AssignmentSubmissionController.java | Staff submission management routes |
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/application/service/AssignmentCommandService.java | Assignment creation, target replacement, self-submission, and grading rules |
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/application/service/AssignmentQueryService.java | Staff queries, student visibility queries, and student-facing submission/result reads |
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/application/service/AssignmentLifecycleService.java | Persists overdue ACTIVE assignments as CLOSED |
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/adapter/in/scheduling/AssignmentDeadlineScheduler.java | Tenant-aware scheduled overdue-assignment close job protected by ShedLock |
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/entity/Assignment.java | Assignment aggregate, workflow flags, visibility flags, and compatibility class_id column |
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/entity/AssignmentTarget.java | Canonical audience rows for CLASS and STUDENT targeting |
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/entity/AssignmentSubmission.java | Submission aggregate for answer maps and exam-result linkage |
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/repository/AssignmentRepository.java | Assignment list queries and bulk overdue close update |
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/repository/AssignmentTargetRepository.java | Visibility lookups and target replacement persistence |
lumie-backend/libs/internal-api/src/main/java/com/lumie/assignment/api/AssignmentService.java | Internal monolith lookup contract |
lumie-backend/libs/internal-api/src/main/java/com/lumie/exam/api/ExamService.java | Internal exam metadata and manual-grading dependency |
lumie-backend/modules/exam/src/main/java/com/lumie/exam/application/service/ResultCommandService.java | Inline manual-answer grading path consumed by EXAM_MANUAL assignments |
lumie-backend/app/src/main/resources/db/migration/public/V81__assignment_redesign_expand.sql | Adds assignment workflow columns, assignment_targets, and exam-linked submission fields |
lumie-backend/app/src/main/resources/db/migration/public/V82__assignment_redesign_tenant_safe_indexes.sql | Adds tenant-safe indexes and composite foreign keys for the redesign |
lumie-backend/app/src/main/resources/db/migration/public/V83__drop_assignment_title_description.sql | Drops legacy assignment title and description columns |
lumie-backend/app/src/main/resources/db/migration/public/V84__drop_assignment_type_and_draft.sql | Drops assignment type and removes the DRAFT status from active workflow |
lumie-backend/app/src/main/resources/db/migration/public/V85__assignment_exam_only_active_guard.sql | Closes non-exam legacy rows and enforces active assignments as exam-backed |
lumie-backend/app/src/main/resources/db/migration/public/V86__assignment_exam_fk_restrict.sql | Changes linked-exam deletion behavior to restrict active assignment breakage |
lumie-backend/app/src/main/resources/db/migration/public/V89__require_assignment_due_date.sql | Backfills legacy null deadlines and requires assignments.due_date |
lumie-backend/app/src/main/resources/db/migration/public/V90__assignment_overdue_close_index.sql | Adds the partial index used by overdue assignment close queries |
Public Surface
| Consumer surface | Entrypoints |
|---|---|
| Staff authoring | POST /v1/assignments, GET /v1/assignments, GET /v1/assignments/{id}, PATCH /v1/assignments/{id}, DELETE /v1/assignments/{id}, GET /v1/assignments/class/{classId} |
| Student dashboard | GET /v1/assignments/me, GET /v1/assignments/{id}/me, PUT /v1/assignments/{id}/me/submission |
| Staff submission management | GET /v1/assignments/{assignmentId}/submissions, GET /v1/assignments/{assignmentId}/submissions/status, POST /v1/assignments/{assignmentId}/submissions, PATCH /v1/assignments/{assignmentId}/submissions/{submissionId}, GET /v1/assignments/{assignmentId}/submissions/student/{studentId} |
| Assignment statistics | GET /v1/assignments/statistics/dashboard |
Internal Surface
AssignmentService still exports only lookup methods:
getAssignment(...);getAssignmentsByClass(...).
The redesign did not turn assignment grading into a separate service boundary.
EXAM_MANUAL grading stays inside the modular monolith through the internal
ExamService.
Aggregates And Tables
| Aggregate | Table | Notes |
|---|---|---|
Assignment | assignments | Stores exam-backed workflow mode, required deadline, status, correct-answer visibility, required linked_exam_id for active rows, and a compatibility class_id alongside the new target rows |
AssignmentTarget | assignment_targets | Canonical audience rows. target_type=CLASS and target_type=STUDENT both use target_id plus tenant RLS |
AssignmentSubmission | assignment_submissions | Unique on (assignment_id, student_id) and stores objective-answer maps, exam_result_id, score/pass fields, and feedback metadata |
AssignmentResponse exposes both the compatibility classId and the new
targetClassIds / targetStudentIds lists. The compatibility column remains
for older class-centric queries, but the student visibility contract now comes
from assignment_targets.
Audience Targeting And Visibility
- At least one target is required. Empty class and student target lists fail
with
TARGET_REQUIRED. CreateAssignmentRequest.classIdis still accepted, then merged intotargetClassIds.AssignmentCommandService.legacyClassId(...)keepsassignments.class_idpopulated by choosing the first class target, or by deriving the first enrolled class for individually targeted students.GET /v1/assignments/meresolves the current student from auth user ID, unions direct-student targets with enrolled-class targets, and returns visible assignments, including closed rows so submitted results remain accessible.GET /v1/assignments/{id}/mehides untargeted assignments behindASSIGNMENT_NOT_VISIBLE. Closed assignments can still be read, butcanSubmitbecomesfalse.GET /v1/assignments/{assignmentId}/submissions/statusexpands every targeted student, then overlays the latest stored submission if one exists. Students without a stored row are reported asPENDING.
Workflow Mode And Grading Rules
- Create and update require a valid
linkedExamId. - Active assignments use
submissionMode=EXAM_MANUALandevaluationMode=EXAM_AUTO.AssignmentCommandService.requireExamManualWorkflow(...)rejects any other active workflow withINVALID_STATUS. CreateAssignmentRequest.dueDateis required. PublicPOSTrequests without it fail request-body validation. Internal create/update paths and the domain aggregate reject null deadlines withDUE_DATE_REQUIRED.- Overdue
ACTIVEassignments are persisted asCLOSED. The scheduler closes them periodically, and single-assignment detail plus submission paths run an assignment-scoped close guard before loading the row so the UI is not dependent on the scheduler interval. - List responses also compute the effective status from
dueDate, so overdue rows appear asCLOSEDin lists even before the scheduler persists the status. - Student and staff submission payloads use
answers, a map keyed by question number as a string. - Grading runs inline through the internal exam module and stores both an
assignment_submissionsrow and anexam_resultsrow in the same request path. - Staff grading by
PATCH /submissions/{submissionId}is blocked when the assignment usesEXAM_AUTO. - The staff authoring UI creates a new exam sheet using the same exam-builder fields as exam management, then stores only assignment-specific deadline, target, status, and correct-answer visibility data on the assignment row.
Source anchor:
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/application/service/AssignmentCommandService.java#submitCurrentStudent
ExamService.ManualGradeResult result = examService.gradeManualAnswers(
TenantContextHolder.getRequiredTenant(),
assignment.getLinkedExamId(),
studentId,
request.answers());
submission.submitExam(request.answers(), result.examResultId(), result.totalScore(),
result.passed(), result.grade() != null ? String.valueOf(result.grade()) : null);
That path is synchronous and in-process. It does not publish RabbitMQ events,
create omr_grading_jobs, upload OMR images, or call lumie-worker's
grading-svc.
Result Visibility Contract
The active assignment contract always exposes score, pass/fail, per-question
type, and per-question correctness. Grade is not used by assignments, so showGrade=false. The only
student-facing result toggle is showCorrectAnswers.
AssignmentQueryService.studentSubmissionResponse(...) keeps score and
pass/fail visible on student-facing reads. AssignmentCommandService.filteredQuestionResults(...)
includes per-item type, correctness, and score, and only includes correctAnswer when
showCorrectAnswers=true.
This means:
GET /v1/assignments/{id}/mereturns the latest assignment-scoped submission and question-result view for the current student when one exists.PUT /v1/assignments/{id}/me/submissionreturns the same visibility booleans alongside the saved submission so the dashboard can decide which fields to reveal immediately.- Feedback is not masked by these flags. It stays visible when present.
Runtime Flows
Staff create or update flow
- Parse status, target IDs, deadline, correct-answer visibility, and the nested exam payload from the request.
- Validate each class target through
ClassServiceand each student target throughStudentService. - Create or validate the linked exam through
ExamService. - Close the assignment immediately if the requested
ACTIVEstatus is already past its deadline. - Save the exam-backed
assignmentsrow, including the compatibilityclass_id. - Delete existing
assignment_targetsrows for the assignment and insert the replacement set.
Student self-submission flow
- Persist the target assignment as
CLOSEDwhen it is overdue and stillACTIVE. - Resolve the current student ID from tenant slug plus auth user ID.
- Reject non-
ACTIVEassignments withASSIGNMENT_CLOSED. - Reject untargeted or hidden assignments with
ASSIGNMENT_NOT_VISIBLE. - Reuse the existing
(assignment_id, student_id)submission row when present; otherwise create it. - Call
ExamService.gradeManualAnswers(...), persistanswersplusexam_result_id, and build assignment-scopedquestionResults. - Return
AssignmentSubmissionResultResponsewith the saved submission and the active visibility flags.
The student PUT /v1/assignments/{id}/me/submission path accepts only the first
submission for the current student. Once an (assignment_id, student_id) row
exists, the same route raises DUPLICATE_SUBMISSION; the frontend removes the
submission form and shows the graded result.
Representative Contract Examples
This request shape matches CreateAssignmentRequest, AssignmentResponse, and
the admin assignment form in
lumie-frontend/src/features/assignment-management/create-assignment/ui/CreateAssignmentForm.tsx.
Staff creates an EXAM_MANUAL assignment
POST /v1/assignments
{
"classId": 12,
"status": "ACTIVE",
"dueDate": "2026-07-15T14:59:00Z",
"submissionMode": "EXAM_MANUAL",
"evaluationMode": "EXAM_AUTO",
"exam": {
"name": "중간고사",
"category": "GRADED",
"gradingType": "GRADED",
"gradeScale": "NINE_GRADE",
"totalQuestions": 2,
"correctAnswers": {
"1": "2",
"2": "4"
},
"questionScores": {
"1": 50,
"2": 50
},
"questionTypes": {
"1": "객관식",
"2": "객관식"
}
},
"targetClassIds": [12],
"targetStudentIds": [301],
"showScore": true,
"showPassFail": true,
"showGrade": false,
"showQuestionResults": true,
"showCorrectAnswers": false
}
{
"id": 88,
"classId": 12,
"dueDate": "2026-07-15T14:59:00Z",
"maxScore": null,
"attachments": null,
"status": "ACTIVE",
"submissionMode": "EXAM_MANUAL",
"evaluationMode": "EXAM_AUTO",
"linkedExamId": 44,
"targetClassIds": [12],
"targetStudentIds": [301],
"showScore": true,
"showPassFail": true,
"showGrade": false,
"showQuestionResults": true,
"showCorrectAnswers": false,
"createdAt": "2026-07-10T03:00:00Z",
"updatedAt": "2026-07-10T03:00:00Z"
}
Student submits answers for an EXAM_MANUAL assignment
This response shape matches SubmitAssignmentRequest,
AssignmentSubmissionResultResponse, and
StudentAssignmentSubmissionResultSchema.
PUT /v1/assignments/88/me/submission
{
"answers": {
"1": "2",
"2": "4"
}
}
{
"submission": {
"id": 410,
"assignmentId": 88,
"studentId": 301,
"content": null,
"attachments": null,
"answers": {
"1": "2",
"2": "4"
},
"examResultId": 902,
"score": 95,
"passed": true,
"gradeValue": null,
"feedback": null,
"submittedAt": "2026-07-10T03:12:00Z",
"gradedAt": "2026-07-10T03:12:00Z",
"status": "GRADED",
"createdAt": "2026-07-10T03:12:00Z",
"updatedAt": "2026-07-10T03:12:00Z"
},
"showScore": true,
"showPassFail": true,
"showGrade": false,
"showQuestionResults": true,
"showCorrectAnswers": false,
"questionResults": [
{
"questionNumber": 1,
"selectedChoice": "2",
"correct": true,
"questionType": "객관식",
"score": 50,
"correctAnswer": null
}
]
}
When showCorrectAnswers=false, questionResults[].correctAnswer is null.
When it is true, the same assignment-scoped result list includes the answer
key.
Dependencies And Boundaries
| Dependency | Why it exists |
|---|---|
ClassService | Validate class targets, list enrolled classes, and derive a compatibility class_id |
StudentService | Validate targeted students and resolve the current student from auth user ID |
ExamService | Validate linked exams, expose exam metadata to the student detail route, and grade manual answers inline |
The assignment module remains a backend module inside the modular monolith. It does not own worker messaging, external OMR HTTP calls, or background grading jobs.
Failure Modes
CLASS_NOT_FOUNDorSTUDENT_NOT_FOUNDwhen any target ID is invalid.TARGET_REQUIREDwhen no class or student target survives normalization.EXAM_NOT_FOUNDwhenEXAM_MANUALis created or updated without a valid linked exam.ASSIGNMENT_NOT_VISIBLEwhen a student reads or submits an untargeted assignment.ASSIGNMENT_CLOSEDwhen a student submits to a non-ACTIVEassignment.INVALID_SUBMISSIONwhen required answers are missing or when staff attempts manual grading for an exam-auto assignment.DUPLICATE_SUBMISSIONwhen a staff or student submission path receives a second submission for the same(assignment_id, student_id).DUE_DATE_REQUIREDwhen an internal create/update path or domain method tries to persist an assignment with a null deadline. PublicPOSTrequests withoutdueDatefail request validation before command handling, andPATCHrequests that omitdueDatepreserve the existing deadline.INVALID_STATUSwhen request enums do not map to assignment, submission, mode, or evaluation enums.JSON_SERIALIZATION_FAILEDwhen JSONB attachment or answer conversion fails.
Observability And Retry Behavior
AssignmentCommandServicelogs assignment create, update, delete, submission create, and submission update events.AssignmentLifecycleServicelogs overdue close events, andAssignmentDeadlineSchedulerlogs the tenant slug plus number of rows changed when a scheduled pass closes overdue assignments.- The student
PUT /me/submissionflow has no queue, worker handoff, or broker retry. EXAM_MANUALgrading is part of the request transaction boundary. Client-side HTTP retry is the only retry mechanism on this surface.- The student route is not retry-idempotent after a successful submission. A second submit for the same assignment and student is rejected before grading or saving.
Verification
./gradlew :modules:assignment:test
./gradlew :modules:exam:test --tests '*ResultCommandService*'
rg -n "assignment_targets|submission_mode|evaluation_mode|linked_exam_id|show_score|exam_result_id|due_date" \
lumie-backend/app/src/main/resources/db/migration/public/V81__assignment_redesign_expand.sql \
lumie-backend/app/src/main/resources/db/migration/public/V82__assignment_redesign_tenant_safe_indexes.sql \
lumie-backend/app/src/main/resources/db/migration/public/V85__assignment_exam_only_active_guard.sql \
lumie-backend/app/src/main/resources/db/migration/public/V89__require_assignment_due_date.sql \
lumie-backend/app/src/main/resources/db/migration/public/V90__assignment_overdue_close_index.sql
cd lumie-document/docusaurus && npm run build
Expected success signals:
- Gradle finishes with
BUILD SUCCESSFUL. - Assignment module tests and
ResultCommandServicetests pass without failures. rghits the redesign columns and tables inV81and the tenant-safe FK/index names inV82.- Docusaurus finishes without MDX or broken-link errors for
backend/assignment-svc.