Learning Management System
An enterprise learning platform for employee training and certification — and a deliberate second attempt at the micro-frontend problem, with the previous platform's lessons applied.
- Client
- Enterprise internal
- Year
- 2024—2026
- Role
- Frontend Engineer
- Team
- Single platform team
Context
Two audiences share one product. Administrators build curricula, approve enrolments and issue certificates. Learners consume material and track progress. The two groups have almost nothing in common in their day-to-day use.
Having already split a platform eight ways, the question here was not whether to use micro-frontends but how few boundaries the product could get away with.
Constraints
- Two distinct audiences
- Admin and learner journeys diverge almost completely — a natural seam that follows users rather than departments.
- Type safety across boundaries
- TypeScript strict mode was non-negotiable, including across federated module boundaries.
- Production observability
- Support needed to reproduce what a specific user saw, and errors needed to surface without waiting for a ticket.
- Design consistency
- One visual system across every module, owned in one place.
Architecture
Four Vue 3 + TypeScript applications wired with Vite Module Federation: a shell host, an admin portal, a learning portal, and a shared design system consumed as a remote.
Shell / host + 4 remotes
- Admin Portal18 modules — governance side
- Learning Portal5 modules — learner side
- Auth & IdentityKeycloak OIDC, impersonation
- Design System53 components, consumed by both portals
- Boundaries follow user role, not department. Admin and learner are genuinely different products sharing a shell.
- The design system is a federated remote rather than a published package, so a component fix reaches every consumer without a coordinated version bump.
- 23 feature modules sit inside those four deployables — 18 on the admin side, 5 on the learner side.
Key decisions
Module Federation instead of Single-SPA
Vite-native, and types can be shared across the boundary — which Single-SPA's runtime import maps never gave us.
The apps are now coupled through their build tooling. Every deployable has to move to a compatible Vite version more or less together.
Four boundaries instead of eight
The previous platform proved that seams drawn along the org chart create coordination overhead without delivering autonomy. Splitting by user role gave real independence.
Individual apps are larger, and builds take longer than the smaller micro-apps did.
Design system as a federated remote
53 components stay in one place and fixes propagate immediately rather than through a release train.
A bad publish is instantly everyone's problem. This trades slow-and-safe for fast-and-blast-radius, and it needs real release discipline to be the right call.
Strict mode and the full lint chain from the first commit
TypeScript strict, ESLint, Prettier and Husky pre-commit hooks are cheap at the start of a codebase and extremely expensive to retrofit.
Slower initial velocity, and some genuine friction while the team calibrated to it.
Keycloak impersonation plus Sentry
Support can see exactly what a user sees, and errors arrive before the ticket does.
Impersonation is a powerful capability sitting inside a banking product. It needs to be audited as carefully as it is useful.
Outcome
The measurable win over the previous platform was not raw performance — it was that fewer boundaries meant less cross-team coordination for the same amount of delivered product.
What I’d revisit
Impersonation shipped before its audit trail was as strong as it should have been. In a banking context, the ability to act as another user needs logging that is at least as rigorous as the feature itself.
A design system consumed as a live remote by four applications should have had visual regression tests from the start. Without them, the blast radius is real but invisible until something breaks.
Diagrams are drawn from scratch and module names describe the domain. No client assets, interfaces or internal naming appear here.