All posts

Rescue your software

One URL Generator Instead of Two Dozen Controllers: File Resolution in a Multi-Tenant SaaS

Why we replaced storage calls scattered across ~23 controllers with one URL generator, and how a placeholder fallback keeps a multi-tenant SaaS up.

MindForge Engineering3 min read

In a multi-tenant SaaS, a file is never just a file. It belongs to a tenant, it may live on local disk or in object storage, and it may not exist at all if a migration is only half finished. On a licensed course-selling platform we re-platformed onto S3-compatible storage, the change that made everything else manageable was replacing scattered storage calls with a single URL-generator abstraction.

The problem with building URLs everywhere

The platform built file URLs wherever it needed them. A course image in one controller, an instructor avatar in another, a downloadable resource somewhere else, each with its own small piece of path logic. Across the application, about 23 controllers handled uploads in their own way.

That pattern has three costs:

  • Every storage change touches everything. Moving to object storage means editing each call site and hoping none were missed.
  • Behaviour drifts. Two controllers that both "show an image" end up handling missing files differently.
  • Partial states break pages. In a multi-tenant system, some tenants' files are migrated before others. Code that assumes one location fails for everyone else.

The abstraction: one place that answers "where is this file?"

We introduced one URL generator that every part of the application uses to turn a stored file reference into a URL. It knows how to resolve a file on local storage and on object storage, so the rest of the code never has to.

Alongside it, uploads were consolidated onto one consistent path. The result was one way to write a file and one way to read it back, instead of roughly two dozen slightly different ones.

This is less about elegance than about risk. With one resolver, the migration can proceed tenant by tenant, and the application keeps working throughout, because the resolver handles both worlds.

Graceful degradation: the placeholder fallback

The detail that mattered most in production was what happens when a file is missing.

In a live multi-tenant platform, a file can be missing from storage for ordinary reasons: a migration batch that has not reached that tenant yet, a record that references something deleted long ago, a copy that failed and needs a retry. Without handling, that becomes a broken image, or worse, an error that stops the page rendering.

The URL generator returns a placeholder when a tenant's file cannot be found in storage. A missing course image becomes a neutral placeholder rather than a broken layout. The platform degrades gracefully instead of failing loudly in front of a tenant's customers, and the missing file becomes a data task for us, not an incident for them.

Upgrade first, migrate second

Before the storage work started, we added a self-update mechanism and brought the platform up to a newer vendor version. Doing it in that order was deliberate:

  • A licensed platform receives vendor updates. Migrating an old version and then upgrading means redoing parts of the migration.
  • Debugging one change at a time is far faster than debugging an upgrade and a storage migration together.

The same session also enforced HTTPS and configured the new host. We describe the upgrade side in more detail in keeping licensed platforms upgradeable.

When this pattern is worth it

A central file resolver is worth building when:

  • Files are referenced from more than a handful of places.
  • You expect to change storage location, CDN or bucket structure at least once.
  • Data belongs to multiple tenants or customers who migrate at different times.
  • A missing file should never take down a page.

If your application only stores a profile picture in one place, a helper function is enough. The broader, step-by-step version of this migration is in our object storage playbook.

A short design checklist

  • One function or service turns a stored reference into a URL. Nothing else builds file URLs.
  • It resolves every storage location the app might be in during a migration.
  • It has an explicit, designed fallback for missing files.
  • Uploads go through one consistent path that pairs with it.
  • The platform is on its current version before the migration begins.

The anonymised project write-up is in our work library. If you are running a platform that has outgrown the way it stores files, that is exactly the kind of problem our software rescue work is for.

Client details in this post are anonymised to respect confidentiality. The engineering described is from the project listed below.

Have something you need to build?

Whether you’re starting with an idea, improving an existing product, or trying to rescue unfinished software, we’ll help turn the next step into a clear plan.

No sales presentation. We start with the problem.