Designing a One-to-One Teaching System: Rolling Enrollment and Personalized Paths

    This article explains how to design a one-to-one teaching platform for a small boarding school with rolling admissions. It covers core domains, a layered event-driven architecture, student profiles, course-draft generation, eventual consistency, ESL rules, the teaching engine, GraphQL teacher dashboards, and practical scaling.

    Why one-to-one systems are harder than class-based ones

    Traditional class teaching can use a shared curriculum, shared pace, and a shared timetable. One-to-one is different. Each student has a dedicated mentor, course paths diverge, and start dates are not fixed. The system must manage dozens of personalized plans, dozens of teacher calendars, and new students who may arrive at any time.

    Three difficulties usually appear. First, personalized content is large; lesson plans and course combinations cannot be maintained by hand one by one. Second, enrollment is rolling, so the system cannot depend on a fixed registration window; new students need a usable plan soon after arrival. Third, students may come from different countries, with large differences in language, time zone, and culture, so courses and notifications must support multiple languages.

    The system described here serves a boarding school that is fully one-to-one. The school is small—typically dozens of students—but it demands high personalization and data security. The rest of this article covers functions, architecture, key modules, and non-functional requirements.

    Clarify the core functional domains first

    Before design, split the business into five domains. Each domain owns a clear set of data and owners.

    1. Student lifecycle: application, admission, enrollment, departure, and alumni. After admission, create an academic record; after departure, keep history for long-term tracking and alumni contact.
    2. Personalized teaching: intake assessment, course customization, lesson scheduling, assignment grading, and progress review. This is the core of the system.
    3. Multilingual and cross-cultural support: ESL banding, bilingual materials, classroom captions, and records of cross-cultural activities.
    4. Community and home–school communication: parent portal, internal messaging, and alumni network. Messages go through sensitive-word filtering; parents need a second factor to view sensitive information.
    5. Operations: housing, finance, and compliance. Housing covers beds and access control; finance covers tuition installments and refunds. These two data sets have the highest consistency requirements.

    Splitting functions sets priority. Phase one should focus on student records, course drafts, the teacher workbench, and parent communication, because they affect daily teaching.

    Architecture: layers plus event-driven flow

    The system uses a frontend–backend split with microservices. Services talk through APIs and a message queue. There are four layers.

    The access layer includes three web views (parents, students, teachers) and a mobile client with push and offline cache. The UI defaults to the user’s language preference and allows a manual switch.

    The application layer includes the teaching engine, scheduling, assessment, communication, finance, and reporting. Each service owns its data. Cross-service processes use events rather than direct database calls.

    The data layer is chosen by data characteristics. Strongly transactional data—students, teachers, courses, and enrollments—goes in a relational store such as MySQL. Unstructured content—personalized lesson plans, learning logs, and assessment reports—goes in a document store such as MongoDB. Frequently read student-profile summaries and course catalogs go in Redis. Video, courseware, and assignment attachments go in cloud object storage.

    The infrastructure layer uses Kubernetes for orchestration, ELK for centralized logs, daily incremental backups, and cross-region disaster recovery.

    Student profile: keep only necessary fields

    The student profile stores multidimensional tags as JSON so it can grow later. A simplified example:

    1. {
    2. "studentId": "S2025001",
    3. "profile": {
    4. "academic": {
    5. "mathLevel": "advanced",
    6. "readingLexile": 1250,
    7. "eslLevel": "B1"
    8. },
    9. "interests": ["robotics", "creative_writing"],
    10. "contact": {
    11. "preferredLanguage": "zh-CN",
    12. "timezone": "Asia/Shanghai"
    13. },
    14. "meta": {
    15. "admissionDate": "2025-03-12",
    16. "expectedGraduation": "2027-06-30"
    17. }
    18. }
    19. }

    Follow minimum necessity. The example has no religion field, because scheduling and housing usually do not need it. If the school truly needs it (for example diet or holidays), collect it as an optional field, state the purpose, and require written guardian consent. Log every read of that field.

    Each field should have a clear owner and update source. Assessment results are written by the assessment service; interest tags are filled by teachers or students; they must not overwrite each other.

    How fast can a course draft be generated, and what is the fallback?

    The new student’s plan is the most urgent part of rolling enrollment. The target is a draft within 24 hours of completed assessment. That target depends on three preconditions: assessment done, teachers scheduled, and profile fields complete. If they are not met, the SLA clock does not run, and the board shows the wait reason.

    Generation has four steps.

    1. Set subject base levels from the intake assessment.
    2. Pull required courses for those levels from the knowledge graph and add electives at a set ratio.
    3. Add interest courses from similar students’ history.
    4. A teacher reviews, adjusts, and submits.
    1. function generateDraft(student):
    2. scores = assessments.latest(student.id)
    3. if scores is missing:
    4. return WAITING("assessment_pending")
    5. required = knowledgeGraph.coreCourses(scores)
    6. elective = recommender.similarCourses(student.id, minSample = 20)
    7. draft = merge(required, elective)
    8. return draft // draft enters teacher review; it does not take effect directly

    Fallback has three levels. First, retry twice on failure with increasing delay. Second, if the recommender is down or the sample is too small, skip collaborative filtering, use template courses only, and mark the draft “no recommendation.” Third, if the draft is still incomplete after 24 hours, alert academic administrators and switch to manual scheduling.

    Collaborative filtering needs history. If fewer than 20 students share the same level and similar interests, skip that step.

    Rolling enrollment: state machine and consistency

    The core is an “enroll anytime” state machine. After application, the record enters a review queue and notifies admissions. After interview and offer, the system creates the student account, assigns a mentor, and pre-generates a “prep pack” (language test link, campus map, culture handbook). On arrival, the student scans a code and all related states activate.

    The admission event triggers a chain: create the record, assign mentor, assign a bed, create a finance bill. These steps live in different services and cannot share one database transaction.

    Use eventual consistency, not strong atomicity.

    First, the admission service writes business data and a local outbox record in one local transaction—both succeed or both fail.

    Second, a relay publishes outbox rows to the queue. Consumers must be idempotent so duplicate messages do not create duplicate data.

    Third, on failure, retry with exponential backoff. After the max retries, mark the flow “needs human handling” and alert.

    Fourth, compensating messages reverse steps that need undo. For example, if housing assignment fails, release the bed and notify admissions.

    For contended resources such as beds and tuition charges, consider Saga or TCC with a coordinator, or transactional messages if the broker supports them (for example RocketMQ). Choose based on operations skill. For a small school, a local outbox plus compensation is usually enough.

    The UI should reflect this. After admission, the student app should show “preparing,” not “complete,” until every step succeeds.

    How to write ESL banding rules

    ESL banding is easy to get wrong. English tests are often adaptive and return CEFR levels (A1–C2). The system uses the level to place courses.

    A corrected example uses strict equality, covers all levels, and handles missing data:

    1. function applyEslRules(student) {
    2. const level = student.profile.academic.eslLevel;
    3. if (level === "A1" || level === "A2") {
    4. enroll("ESL_Foundation");
    5. delay("Literature", { months: 4 });
    6. } else if (level === "B1") {
    7. enroll("ESL_Advanced");
    8. parallel("History");
    9. } else if (level === "B2") {
    10. enroll("ESL_Academic");
    11. } else if (level === "C1" || level === "C2") {
    12. optional("ESL_Writing");
    13. } else {
    14. flagForReview(student.id, "missing_esl_level");
    15. }
    16. }

    Handle two edges. First, scores that sit between two levels should not auto-place; they go to human review. Second, a delayed course must not exceed remaining teaching weeks in the term, or the student would finish the term still missing a core subject.

    For a small school, store rules as JSON loaded at startup so changes do not need a code release. When rules grow and need complex priority, consider a rules engine such as Drools.

    Teaching engine: rules and models together

    The engine recommends paths on the knowledge graph from the student profile. Start with rules; add adaptive models later.

    Rules handle deterministic decisions, for example “A2 students must take ESL foundation first.” Academic staff own the rules; each rule has an owner and effective date.

    Models rank and fill recommendations, for example ordering electives by interest after requirements are met. Model output is advice only; teachers always decide.

    In cold start, new students have no history, so use assessment and teacher templates only and turn collaborative filtering off until there is enough sample.

    Human review sits at three points: before draft submit, on course replacement, and mid-term changes. Every manual change records a reason; those records later improve rules and models.

    Useful metrics:

    • Coverage: share of enrolled students who get a draft; target near 100%.
    • Teacher adoption: share of drafts submitted unchanged. Too low means weak recommendations; too high may mean weak review.
    • Schedule-change rate: in-term timetable changes divided by student count. Persistently high usually means weak initial constraints.
    • First-generation time: P90 from assessment complete to usable draft, to watch the SLA.

    Teacher workbench: GraphQL and N+1

    The workbench shows today’s timetable, each lesson’s student profile, last-lesson feedback, and pending assignments. This page easily hits N+1: one timetable query, then per lesson queries for student, feedback, and work.

    GraphQL does not fix this by itself. It only provides query syntax; resolvers can still hit the database per field. Three common fixes:

    First, DataLoader for batching: collect student lookups in one request and query by ID in one batch.

    Second, paginate list fields, cap page size, and limit query depth and complexity.

    Third, a BFF that assembles the dashboard in one shot, which fits a stable page.

    Example query:

    1. query TeacherDashboard($teacherId: ID!, $date: Date!) {
    2. teacher(id: $teacherId) {
    3. schedule(date: $date) {
    4. lessonId
    5. startTime
    6. student {
    7. id
    8. profileSummary {
    9. mathLevel
    10. eslLevel
    11. interests
    12. }
    13. }
    14. lastFeedback {
    15. summary
    16. createdAt
    17. }
    18. pendingAssignments(first: 20) {
    19. id
    20. title
    21. submittedAt
    22. }
    23. }
    24. }
    25. }

    The student resolver should use a DataLoader keyed by student ID so one request hits the student table once. lastFeedback and pendingAssignments need batching as well. After quick scores and comments, the notification service pushes to parents and the teacher UI shows delivery status.

    Performance and scale

    Daily concurrency is low for a small school. Real load comes from intake peaks and parent-open days, when traffic can be several times normal.

    Do not scale on CPU alone. Watch P95 latency, queue backlog, database connections, and requests per second. For example, add replicas when P95 stays over threshold for five minutes or the queue exceeds a set length. Set a minimum replica count (at least 2 for availability) and a maximum (to control cost).

    For this size of school, heavy autoscaling adds ops burden. A practical approach is to scale up before intake peaks and warm caches; autoscaling is a backstop.

    Read replicas can keep reporting off the primary. Hot data such as the day’s timetable and student-profile summaries should sit in cache.