DEV Community

Johnston Kweku Abubakar
Johnston Kweku Abubakar

Posted on

I spotted the same database design pattern three times in my own code today


I'm building EduTrack, a Django-based school management system for Ghanaian primary and JHS schools. Today's session covered a mix of real bug fixes and one design realization worth writing about.
The bugs first, briefly:
A permission check in my attendance-marking view could crash if a teacher's class assignment changed between page load and form submission — a genuine race condition I found by deliberately testing it: marking attendance as a teacher, then switching to an admin account mid-flow to unassign that teacher from their class, then submitting. Confirmed the crash, fixed it with a proper try/except around the lookup.
Separately, I had two NameError bugs hiding in conditional branches — variables referenced in a context dict that were only ever assigned inside an if block, with no guarantee that branch had run. Both fixed by tracing every possible path through the function by hand rather than just trusting that "it worked when I tested it once."
The pattern realization:
The part I want to actually write about: I looked at my Result model — which stores one row per student per subject per term, with a class_score capped at 30 and exam_score capped at 70 — and felt that something was structurally wrong with it. I couldn't fully articulate why at first.
The problem: that model only represents a final result per term. It has no way to represent the individual quizzes and class tests that happen during a term. Where does a 10-mark quiz go? It doesn't fit.
What I was actually reaching for, without naming it yet, was the header/line-item pattern — split one "event" into a parent record describing the event itself, and child records describing individual outcomes of that event, each linked back via a foreign key.
I'd already used this pattern twice before in the same project:

Fee (parent: what a class owes this term) → FeePayment (child: each individual payment)
Attendance (parent: a class's attendance session for a given day) → AttendanceRecord (child: each student's present/absent status)

Today, completely unprompted, I recognized the same shape forming again for assessments:

Assessment (parent: a specific quiz/test — subject, date, max score) → AssessmentResult (child: each student's score on that specific assessment)

Why this pattern matters:
Without it, you get redundant metadata copied across every row (subject, date, max score repeated 35 times for a class of 35), ambiguity about which rows belong to the same "event," and header-level data (who recorded it, what type of test) awkwardly crammed into a per-student table where it doesn't conceptually belong.
The lesson I'm taking from this: pattern recognition compounds. The first time you solve a structural problem, it's a one-off fix. The second time you recognize you're solving the same problem, it becomes a pattern. The third time, it becomes something you reach for proactively — before the pain forces you to.
Still building, still testing edge cases by hand, still learning out loud.

Top comments (0)