The BFF contract
The dashboard's contract was modelled around the questions a learner or supervisor was actually asking, not around the source systems' schemas. A "learner overview" endpoint returned everything a supervisor needed to see for one learner across all three systems, in one shape, at one freshness floor. A "course progress" endpoint returned the dashboard's view of a course, with deep link references where Beehive owned the underlying content.
Source-system schemas never reached the front end. That was a deliberate constraint, not an accident. Every time a feature added a new field to the BFF response, the question "where does this come from, and how fresh is it" was answered once, at the BFF or progress engine boundary, and then forgotten by the UI.
Progress engine responsibilities
The progress engine owned three things the BFF deliberately did not:
- The consolidated learner record across Campus, OneFile and Beehive references.
- The data freshness model: which fields are real-time, which are batch-ingested, and how stale a given response is allowed to be.
- The progress calculations themselves, which depended on period-based benchmarks, off-the-job hours, e-portfolio assessments and breaks. Those calculations had been validated by the partner organisation in a separate Power BI proof of concept, and the engine's job was to honour the same logic against the consolidated data.
Mixed freshness, honestly handled
The most uncomfortable architectural question was what to do when a real-time field and a 24-hour-stale field appeared next to each other in the same UI tile. The answer was to surface the freshness floor at the API boundary and let the UI present it honestly. A "last updated" stamp at the tile level, a soft warning when the freshness floor was outside expectations, and no attempt to pretend that the OneFile feed was anything other than overnight.
This is where I think a lot of consolidation projects quietly go wrong. The temptation is to hide the messy freshness model behind a clean UI, and then have the team trying to debug why a learner "completed" a module yesterday but it has not appeared on the dashboard yet. Putting the freshness model in the data contract took five minutes to design and saved a category of support questions that would otherwise have lived forever.
Identity bridging
Auth0 owned the dashboard authentication. SAML single sign-on connected the dashboard to Beehive. OneFile and Campus identifiers were mapped to the dashboard's learner identity at ingest time, in the progress engine, so the front end and the BFF only ever spoke the dashboard's identity model. The three source systems' identities never escaped the consolidation layer.
What I would change next time
The split between BFF and progress engine was right for this team and this engagement, but it is a pattern that earns its keep with size. For a smaller team or a smaller integration surface, I would consider collapsing the BFF into the progress engine and letting the front end talk to a single backend service. The principle (one consolidated, freshness-aware contract) is the load-bearing part. The two-process split is an implementation detail driven by team shape and operational concerns, not by the architecture being correct.