Capstone: Design a Global Multi-Tenant Learning Platform

Published by Programming Line in System Design and Distributed Systems. Last updated .

Purpose and Target Outcome

This capstone pulls together capacity planning, data modeling, API design, reliability, and operational decision-making into one concrete system: a global learning platform used by schools, companies, instructors, and learners. The platform must host courses, enroll learners, stream lessons, track progress, grade assessments, issue certificates, and let each customer administer its own users, branding, permissions, and policies.

The central design problem is multi-tenancy. A tenant might be a university, a company, a training marketplace partner, or a government program. Tenants share the same product, but they cannot leak data, interfere with each other’s performance, or force every other tenant to accept their custom rules. A good design gives small tenants cheap shared infrastructure, gives large tenants stronger isolation when needed, and keeps the core learning workflow consistent across regions.

Core Mechanism: Tenants, Regions, and Learning Events

The system has four primary runtime planes. The request plane handles browser and mobile traffic. The learning plane serves course catalogs, lessons, enrollments, quizzes, assignments, and progress. The media plane stores and delivers video, documents, captions, thumbnails, and downloadable assets. The analytics plane consumes learning events and produces dashboards, recommendations, completion records, and audit exports.

A typical request starts at a global edge endpoint. The edge identifies the tenant from a custom domain such as training.example.edu, a path prefix such as /t/acme, or an organization identifier carried after login. The tenant directory returns tenant metadata: status, home region, enabled features, data residency constraints, identity provider settings, plan limits, and isolation tier. The gateway then routes the request to the correct regional application cluster.

The most important internal rule is that every durable learning object is scoped by tenant. Course, module, lesson, enrollment, attempt, grade, certificate, and audit records all include tenant_id. Shared reference data, such as the list of supported languages, can remain global. User identity is usually split: a global account record represents the human, while a tenant membership record represents that person’s role inside one tenant. This prevents one email address used at two organizations from accidentally sharing progress, roles, or permissions.

The write path should be event-centered. When a learner watches a lesson, submits a quiz, or completes a module, the transactional service stores the authoritative state and emits an immutable event such as LessonViewed, QuizAttemptSubmitted, or EnrollmentCompleted. Consumers update search indexes, analytics tables, notification queues, and recommendation models. This separation keeps the learner-facing path fast while letting reporting and automation catch up asynchronously.

API and Configuration Anatomy

Tenant configuration is usually read far more often than it changes, so it belongs behind a small cache with explicit invalidation. Configuration should be declarative rather than scattered through application code. A tenant record can include the routing region, storage policy, authentication mode, enabled modules, rate limits, and isolation tier.

TENANTS = {
    "acme": {"region": "us-east", "tier": "shared", "max_rps": 300},
    "globex": {"region": "eu-west", "tier": "dedicated-db", "max_rps": 1200},
}


def route_request(host, path):
    tenant_slug = host.split(".")[0]
    tenant = TENANTS.get(tenant_slug)
    if tenant is None:
        return {"status": 404, "reason": "unknown tenant"}
    if tenant["tier"] == "dedicated-db":
        database = f"tenant_{tenant_slug}_{tenant['region']}"
    else:
        database = f"shared_learning_{tenant['region']}"
    return {"status": 200, "tenant": tenant_slug, "region": tenant["region"], "database": database}


print(route_request("acme.learn.example", "/courses"))
print(route_request("globex.learn.example", "/courses"))

The expected behavior is that acme routes to a shared database in us-east, while globex routes to its own database in eu-west. The application code can use the same API surface for both tenants because the routing layer selected the storage target before the repository layer runs.

Worked Example 1: Shared-Schema Tenancy

The simplest model puts many tenants in the same logical database tables and includes tenant_id in every tenant-owned table. This works well for thousands of small tenants because connection pools, migrations, indexes, and backups are centralized. The risk is accidental cross-tenant access if a query forgets its tenant predicate.

ENROLLMENTS = [
    {"tenant_id": "acme", "user_id": "u1", "course_id": "python-101", "progress": 80},
    {"tenant_id": "acme", "user_id": "u2", "course_id": "python-101", "progress": 20},
    {"tenant_id": "globex", "user_id": "u1", "course_id": "security-101", "progress": 60},
]


def list_enrollments(tenant_id, user_id):
    return [
        row for row in ENROLLMENTS
        if row["tenant_id"] == tenant_id and row["user_id"] == user_id
    ]


print(list_enrollments("acme", "u1"))
print(list_enrollments("globex", "u1"))

The two calls return different records even though the same user_id appears in both tenants. That is intentional. Tenant membership, enrollment, and progress are tenant-scoped, so a learner’s corporate compliance course at one company cannot appear in another company’s admin report.

Worked Example 2: Regional Placement

Global learning systems need regional routing for latency and data residency. Course metadata might be globally replicated, but learner records, grades, and audit logs often have to stay in a tenant’s home region. A practical design separates globally cacheable content from regulated tenant state.

def choose_region(tenant_policy, user_country):
    if tenant_policy.get("residency") == "EU":
        return "eu-west"
    if tenant_policy.get("residency") == "US":
        return "us-east"
    if user_country in {"JP", "KR", "SG", "AU"}:
        return "ap-southeast"
    return "us-east"


print(choose_region({"residency": "EU"}, "DE"))
print(choose_region({}, "SG"))
print(choose_region({"residency": "US"}, "FR"))

The deterministic outputs are eu-west, ap-southeast, and us-east. The third case shows that tenant policy overrides the learner’s current location. A French traveler using a US-only corporate tenant should still write grade and progress data to the US region if that is the tenant’s contractual and compliance boundary.

Worked Example 3: Event-Driven Progress

Learning progress is not just a percentage column. It is derived from lesson views, quiz attempts, assignment submissions, instructor overrides, and content version changes. The system should record fine-grained events and then compute the current progress state from rules that can evolve.

EVENTS = [
    {"type": "LessonCompleted", "lesson": "l1"},
    {"type": "LessonCompleted", "lesson": "l2"},
    {"type": "QuizPassed", "quiz": "q1"},
]


def completion_percent(events, required_lessons, required_quizzes):
    lessons = {event["lesson"] for event in events if event["type"] == "LessonCompleted"}
    quizzes = {event["quiz"] for event in events if event["type"] == "QuizPassed"}
    completed = len(lessons & set(required_lessons)) + len(quizzes & set(required_quizzes))
    total = len(required_lessons) + len(required_quizzes)
    return round((completed / total) * 100)


print(completion_percent(EVENTS, ["l1", "l2", "l3"], ["q1"]))

The output is 75. Three of four required items are complete. This model makes retries, audits, and reprocessing easier. If a course author later marks l3 optional, the projection can be rebuilt without fabricating historical activity.

Design Choices and Trade-Offs

Shared-schema tenancy is cost efficient and operationally simple, but it demands strict query enforcement, row-level authorization, and careful noisy-neighbor controls. Database-per-tenant improves blast-radius control and custom backup handling, but it increases migration, connection management, and fleet automation complexity. Dedicated deployment per tenant gives the strongest isolation for regulated or very large customers, but it slows feature rollout and raises infrastructure cost.

Course content has a different isolation profile from learner state. Videos, transcripts, and static lesson files should be served through object storage and a content delivery network. Tenant-private files need signed URLs and tenant-aware authorization before the URL is issued. Public marketplace previews can be cached globally. Grade records, assessment attempts, personally identifiable information, and audit logs should stay in regional transactional stores with narrower access paths.

Search and analytics are useful but should not become the source of truth. A search index may contain denormalized course titles and lesson summaries for fast lookup. If it falls behind, learners should still be able to open a course from the transactional catalog. Dashboards can tolerate short lag, but certificate issuance and compliance completion should read from authoritative progress state.

Failure Modes and Troubleshooting

Symptom: an administrator sees learners from another organization in an enrollment export. Likely cause: an export query joined enrollment and user tables without carrying tenant_id through the join. Diagnostics: inspect the query plan, check audit logs for the export request, and reproduce with two tenants containing the same user identifier. Correction: enforce tenant predicates in repository helpers, add tests with colliding IDs across tenants, and consider database row-level security for shared tables.

Symptom: learners in Asia can stream videos quickly but quiz submissions feel slow. Likely cause: media is cached at the edge while assessment writes still travel to a distant home region. Diagnostics: compare edge cache hit rate, API latency by route, and database commit latency by region. Correction: keep regulated writes in the required region, but move read-only assessment metadata closer, reduce synchronous grading work, and return submission acceptance before expensive analytics updates run.

Symptom: one tenant’s semester launch causes timeouts for unrelated tenants. Likely cause: shared workers, database pools, or message partitions let a high-volume tenant monopolize capacity. Diagnostics: break down queue lag, database connections, and request rate by tenant_id. Correction: add tenant-aware rate limits, partition queues by tenant tier, reserve capacity for premium tenants, and promote very large tenants to a stronger isolation tier.

Security, Performance, and Reliability Implications

Authentication proves who the user is; authorization decides what that user can do inside a tenant. The platform should evaluate both tenant membership and resource ownership on every privileged action. Instructor, learner, tenant admin, support admin, and platform operator permissions should be separate roles. Support tools need explicit tenant selection, audited access reasons, and time-limited elevation.

Performance hinges on separating hot paths from derived work. Lesson reads, progress writes, and quiz submissions are hot. Recommendation refreshes, weekly reports, email campaigns, and warehouse exports are derived. The hot path should use bounded synchronous calls and idempotent writes. Derived systems should consume events, retry safely, and expose lag metrics.

Reliability planning should distinguish between tenant-local and platform-wide failures. If one tenant’s dedicated database is down, the platform should avoid failing other tenants. If the tenant directory is unavailable, cached tenant configuration can keep existing tenants online for a short period, but onboarding and configuration changes should pause until the directory recovers.

Hands-On Lab: Model Tenant Routing and Progress

Prerequisites: a Python interpreter and a terminal. No external services are required. The lab simulates tenant routing and progress computation so you can reason about isolation before choosing a real database or cloud provider.

  1. Create a scratch file named learning_platform_sim.py.
  2. Paste the routing example and the progress example into the file.
  3. Add a third tenant with tier set to shared and region set to ap-southeast.
  4. Add one more required lesson to the progress calculation and run the script.
  5. Change one event from LessonCompleted to LessonViewed and run it again.

Verification: the new tenant should route to a shared database in its configured region. The completion percentage should decrease when you add a requirement, and it should decrease again when a completion event no longer matches the accepted event type. This verifies that routing is policy-driven and progress is rule-driven rather than hard-coded.

Cleanup: delete the scratch file when done. If you ran the lab inside a repository, do not commit the scratch file unless it has been intentionally converted into a test or design artifact.

Assessment Exercises

  1. A tenant asks for custom certificate rules while remaining on shared infrastructure. Which parts of the system should be configurable, and which should stay common to all tenants?
  2. Design a database indexing strategy for the enrollment table. Explain which queries need tenant_id first in the index and why.
  3. A large customer needs European data residency, single sign-on, and guaranteed launch-day capacity. Choose an isolation tier and justify the operational cost.
  4. Progress dashboards are delayed by twenty minutes, but learners can still complete courses. Is this an outage, degradation, or acceptable lag? Define the service-level signal you would use.
  5. Explain how you would test that a support administrator cannot accidentally export data from the wrong tenant.

Summary

A global multi-tenant learning platform is designed around scoped tenant identity, regional placement, durable learning records, and asynchronous learning events. The hardest choices are not whether to use a database, cache, queue, or CDN; they are where tenant boundaries are enforced, which data must stay regional, which workloads can lag, and when a tenant deserves stronger isolation. A strong capstone design makes those choices explicit and shows how the platform continues serving learners when one tenant, region, queue, or derived system has trouble.