How immutable submission revisions and an expected-version check prevent a grading form from silently applying feedback to a student's newer answer.
A learner uploads an assignment. An instructor opens the answer and starts reviewing it. While the instructor is writing feedback, the learner finds a mistake and uploads a corrected file.
What should happen when the instructor clicks "Save grade"?
If the application simply updates the current assignment row, the resulting grade may describe a version the learner can no longer see. Nobody needed to act maliciously. Two legitimate users worked with different snapshots of the same record.
This is a familiar concurrency problem, but educational workflows make its impact unusually visible. The answer, the attached files, the feedback, and the published result must remain understandable to learners, instructors, and sometimes guardians or administrators.
Model a submission as an event, not just a mutable field
A naive model stores the latest answer and the latest grade on an assignment:
- assignment ID;
- answer text;
- uploaded file IDs;
- grade;
- instructor comment.
When a new answer overwrites the old one, the instructor loses the evidence of what was originally reviewed. A better model keeps immutable or append-only submission revisions linked to a stable assignment ID.
A submission revision can contain its sequence number, answer text, the set of selected file identifiers, and the time of submission. A new revision is created only when the learner actually submits a changed answer. Existing revisions remain available to authorized participants.
This does not automatically solve concurrent grading. It gives the server something precise to compare.
Carry the version the instructor saw
When a grading screen loads, return the revision identifier alongside the content. The client should send that identifier with the grading request as an expected revision.
Suppose the instructor opened revision 4. The learner then submitted revision 5. If the grading request still says expected revision 4, the server must reject it rather than silently assign the grade to revision 5.
The server response should explain that the submission changed and invite the instructor to reload. Crucially, the client can preserve the unsaved feedback locally while refreshing the work under review.
This is optimistic concurrency: most reviews proceed without a conflict, but the server detects a stale decision when one actually happens.
An optional expected-revision field weakens the guarantee. If older clients can omit it, the API cannot assume every grading request protects against stale content. During migration, document which callers supply it and what the server does when it is absent.
The comparison must be atomic
A common implementation error is:
- Read the current revision.
- Check that it matches the expected value.
- Write the grade.
If another request updates the revision between steps 2 and 3, the check was correct at the time it ran but no longer protects the write.
The comparison and the resulting state change need one concurrency boundary. Depending on the database design, that may be a transaction with a row-level lock or a conditional update that succeeds only when the revision still matches.
A row-level lock serializes competing changes to the same assignment while allowing unrelated assignments to proceed. It does not mean locking the entire class or all learners.
Be careful about attachments. Comparing only answer text is insufficient when revision 5 replaced one photograph with another but kept the same comment. Treat the submitted file set as part of the revision.
Grading and publishing are separate product decisions
Some learning products publish a grade immediately after an instructor completes the check. Others allow grades to be saved as drafts and published later by an authorized user.
Neither model is universally correct. The problem arises when a user interface suggests one model while the server implements the other.
A button labeled "Grade and publish" is clear if the result becomes visible immediately. In a draft-based workflow, the system should show the unpublished state and make publication a distinct action.
Corrections after publication deserve their own history. Store who changed the grade, when, the old and new values, and the reason. If a guardian acknowledged an earlier grade, do not silently treat that acknowledgement as consent to the corrected result.
How to test the race
Use two independent browser sessions and a harmless sample assignment:
- In the instructor session, open revision 4 and start writing feedback.
- In the learner session, submit revision 5 with a changed attachment.
- In the instructor session, attempt to grade using expected revision 4.
- Confirm that the server detects a conflict and does not publish a grade for the wrong revision.
- Reload revision 5, review it, and complete grading through the intended publication path.
Repeat with a client that omits the expected revision value. If that request is allowed, record the remaining risk instead of assuming every client receives the stronger guarantee.
You can also test a repeated identical submission to ensure idempotence and check that two simultaneous grading requests do not create conflicting published results.
The actual invariant
The invariant is not "the latest row has a grade." It is "each grade can be explained by the exact work the instructor reviewed." Revision identifiers, server-side concurrency control, preserved attachments, and visible publication states are means to enforce that invariant.
Once the system can answer "which answer was this grade about?", disputes become easier to investigate and ordinary corrections stop looking like corrupted records.
This is a general technical tutorial, not a product advertisement or a claim about a live installation. The draft requires independent technical verification before public posting.
Top comments (0)