InterviewLegend

Design an Enterprise System

Commonly asked
Alex Chen26 min readUpdated 86 real interview reports
In this lesson
  • Design a multi-tenant system of record in order, starting with tenant isolation and authorization
  • Choose a tenant isolation model, scope roles to tenants, and keep history with effective dates
  • Keep fan-out writes consistent, contain noisy tenants, and keep an append-only audit trail

Understanding the prompt

"Design an enterprise system" is deliberately broad. The first mistake is treating it as a consumer-scale prompt in disguise and sizing a cache tier for a billion-user feed. Enterprise software works differently. One tenant is a whole company, and tenants number in the thousands, nowhere near billions. The value lives in data that is correct, private, and auditable. Surviving a viral spike matters little. So before naming a class or drawing a box, decide what kind of system you're building. It's a system of record that many separate organizations use through one shared codebase. Each sees only its own world and configures that world differently. Each expects a number entered today and a number from three years ago to be retrievable and trusted.

Build the general shape first and layer the company flavor on afterward. A candidate who can only recite a Workday-specific answer but cannot design a multi-tenant SaaS from scratch fails the moment the interviewer swaps the domain from payroll to something else. So the plan here is two passes. The first pass is the reusable framework that holds for any enterprise SaaS. Isolate tenants and gate every request through identity and authorization. Model a core domain service, and choose storage and a consistency posture. Move the slow work off the request path. The second pass adds what makes enterprise software its own animal, and where Workday sits inside it. That means tenant configurability, effective-dated history, audit and compliance, and the integration surface every large customer demands.

The Workday angle
Workday sells systems of record for people, pay, and money. The design conversation rewards modeling the business over surviving a spike. Frame the problem that way out loud in the first minute. Say that the entities and their relationships are the product. One tenant is an entire company and must never see another's data. History is a requirement, because payroll and headcount are legal records you must be able to reconstruct as of any past date. Then design in that order: isolation, identity, the domain model, storage, and the slow work. Two habits mark someone who has built enterprise software beyond a side project. Treat configurability as a first-class requirement, because the same codebase has to bend to a bank and a hospital without a fork. And make the audit trail part of the schema from the start. In this domain, customers pay for knowing who changed what and when it took effect.

Pinning the requirements first

State the functional surface plainly so the design has edges. Many independent organizations, the tenants, share one running system. A user signs in and can only ever act inside their own tenant. Inside that tenant they read and write business objects, the domain records the product is about, and those objects have a shape the tenant itself gets to configure. Every change is attributed to a person and stamped with when it happens and when it takes effect. Slow work, a payroll run, a headcount rollup, a report over the whole population, runs in the background and must not block the interactive request. That is the core, and it is enough to build something real without drifting into a feature tour.

Then name the non-functional bar, because in enterprise software it shapes the whole design. Tenant isolation has to be airtight: a query that forgets its tenant filter and returns another company's salaries is not a bug, it is a breach that ends a contract. Authorization is per-tenant and role-based, so the same person can be an administrator in one context and a plain viewer in another. Data is durable and consistent for records that are money and law, and history is never destructively overwritten. And the system stays predictable under a noisy neighbor, one tenant kicking off a huge job cannot starve everyone else. Say which of these you will treat as hard invariants, and you have set the frame the rest of the design defends.

  • What the round grades: model the business. Clean entities and relationships, honest isolation, and correct history. Not requests-per-second math. If you find yourself sharding a cache before you have named the domain objects, you have misread an enterprise prompt.
  • A tenant is a whole company: thousands, not billions. Scale is measured in tenants and in objects per tenant. There's no global user firehose. That shifts the hard problems from raw throughput to isolation, configurability, and correctness of the data each tenant owns.
  • Isolation is an invariant: never cross tenants. Every read and write carries a tenant and is filtered by it. A missing tenant filter that leaks one company's data into another is the worst failure this kind of system can have. Design for it from the first line instead of a later hardening pass.
  • History is the product: keep the past, dated. Records are effective-dated: a change knows when it takes effect and the prior value is never erased. Payroll and headcount must be reconstructable as of any past date, so the store versions records rather than overwriting them in place.

The reusable framework

Any multi-tenant enterprise SaaS comes down to a small stack of decisions, made in a fixed order because each one constrains the next. Isolation comes first because it colors the schema, the queries, and the deploy model. Identity and authorization come next because nothing else is allowed to run until a request is bound to a tenant and a set of permissions. Only then does the domain service matter, and only after that do storage, consistency, and the slow background work. Walk the interviewer down this ladder and the design stays coherent no matter which domain they hand you.

Tenant isolation

Decide how one running system keeps thousands of companies apart: a shared schema with a tenant key on every row, a schema per tenant, or a database per tenant. This choice sets the blast radius of a bug and the cost of onboarding a new customer, so it is made first and everything downstream inherits it.

Identity and authorization

Put a gateway in front that authenticates the user, resolves which tenant they are acting in, and checks a role-based permission on the specific resource before any handler runs. Authorization is per-tenant, so the same person carries different powers in different contexts.

The business-object service

The core domain service owns reads and writes of business objects. In enterprise software, a tenant sets the shape of an object as configuration. No developer hard-codes the columns. So the service validates against a per-tenant model instead of a fixed schema.

Storage and consistency

Pick a durable transactional store for the objects, keep writes strongly consistent because they are money and law, and version records by effective date so history survives. Reporting reads a separate store so a heavy analytical query never contends with interactive writes.

Background jobs and reporting

Move slow, fan-out work, a payroll run, a headcount rollup, a population report, off the request path onto a queue and idempotent workers. The interactive write returns fast and emits an event; the heavy computation happens asynchronously and lands in the reporting store.

Scale and failure

Scale is per-tenant, so the work is about fairness and blast radius. Isolate a noisy tenant so one customer's giant job can't starve the rest, and cap work per tenant. Make every background step retry-safe, so a crash mid-run doesn't double-pay anyone.

Decision one: tenant isolation

Isolation is the decision the whole design is built on, so make it explicitly and name the trade-off. There are three common models. A shared schema keeps every tenant's rows in the same tables with a tenant_id column on each, and every query filters by it. With schema-per-tenant, each tenant gets its own set of tables inside one database. Database-per-tenant gives each its own database entirely. They trade along the same axis in opposite directions. The more you share, the cheaper it is to run and the easier to onboard a new tenant. The wall between customers gets thinner, though, and one query that forgets its filter costs more. Separate more, and the wall gets stronger. A per-tenant restore or a regional data-residency requirement gets simpler. You carry more operational weight per customer in return.

With thousands of tenants, many of them small to medium, the shared-schema model is the usual default. A schema or database per tenant, multiplied by thousands, is an operational burden no one wants to carry. But sharing puts the entire isolation guarantee on one fragile thing: the tenant filter on every query. The design has to make that filter impossible to forget. Don't rely on every developer remembering it. Push the tenant into the connection or session as a context the data layer injects automatically. Then a handler physically can't issue an unscoped query. Postgres row-level security is one concrete way to enforce it at the database instead of in application code. Even a hand-written query gets filtered by the tenant on the session. State that mechanism out loud. "We filter by tenant_id", with no word on how you stop a forgotten filter, is the answer that leaks data in production.

  • Shared schema, tenant key: cheapest to run. One set of tables, a tenant_id on every row, a filter on every query. Onboarding a tenant means adding a row, with no migration. The whole isolation guarantee rests on the filter, so it must be enforced by the data layer or row-level security, never left to developer discipline.
  • Schema per tenant: a middle ground. Each tenant gets its own tables in one database. A stronger wall than a shared key and per-tenant backup is cleaner, but thousands of schemas strain migrations and connection pooling, so it suits fewer, larger tenants.
  • Database per tenant: strongest wall. Full separation, the simplest per-tenant restore and the cleanest answer to data-residency and a single tenant's regulatory demands. It costs the most per customer, so it is reserved for the largest or most sensitive accounts, often as a premium tier.
  • The onboarding question: how a tenant is born. Interviewers probe provisioning: a new tenant needs its isolation set up, its configuration seeded, and its first admin created. In shared-schema that is inserting rows; in database-per-tenant it is standing up infrastructure. Name which and the cost is honest.

Isolation that holds

  • Binds the tenant to the request context at the gateway and lets the data layer inject the filter, so a handler cannot issue an unscoped query
  • Enforces the boundary in the database with row-level security, so even a hand-written query is scoped to the session's tenant
  • Names the isolation model and its cost on purpose: shared schema for many small tenants, a dedicated database for the few that demand it
  • Treats a cross-tenant read as a breach, worse than any bug, and designs the test that proves one tenant can never see another's rows

Isolation that leaks

  • Says "we add a tenant_id and filter by it" with no mechanism that stops a developer from forgetting the filter
  • Puts the tenant only in application code, so one raw query or one background job runs unscoped and leaks
  • Reaches for a database per tenant at thousands of tenants without pricing the operational weight
  • Treats onboarding as an afterthought, so a new tenant has no defined path to its config and first administrator

Decision two: identity, authz, and RBAC

Nothing in the system runs until a request is bound to a real user, a specific tenant, and a set of permissions. A gateway sits in front of every service and does three things in order. It authenticates the caller, usually by validating a signed token from the tenant's identity provider. Large enterprises bring their own single sign-on and expect SAML or OIDC federation, with no password stored in your system. Next it resolves which tenant the request acts in, from the token and the route, and pins it into the request context. Then it checks authorization. Does this principal, with these roles, have permission to take this action on this specific resource? The resource itself belongs to a tenant. Only if all three pass does the request reach a domain handler.

Authorization is role-based. Candidates most often skip the fact that permissions are scoped. A person isn't simply an administrator. They're an administrator within one tenant, and often within one part of it. So the check has two layers, and the order matters. The outer layer is the tenant match. Even a perfectly valid role gets denied if the resource belongs to a different tenant. Tenant mismatch is a hard deny before any role math runs. The inner layer resolves the principal's roles to the actions they grant on the object's type. If any role grants the action, it's allowed. Tenant isolation as the outer gate stops a crafted request from using a real role in one tenant to reach a resource in another.

java
// Authorization resolves a principal's ROLES to a permission on a resource that
// is always scoped to one tenant. The tenant check is the OUTER gate: even a
// valid role must never reach across tenants, so a tenant mismatch is a hard
// deny BEFORE any role math runs.
enum Action { VIEW, EDIT, APPROVE }
record Principal(String userId, String tenantId, Set<String> roles) {}
class AccessPolicy {
// For this tenant: role name -> the actions it grants on an object type.
private final Map<String, Set<Action>> grants;
boolean allows(Principal p, BusinessObject obj, Action action) {
// 1. Tenant isolation is checked first and on its own. A resource in
// another tenant is denied no matter how privileged the principal is.
if (!p.tenantId().equals(obj.tenantId())) return false;
// 2. Any one of the principal's roles that grants this action is enough.
for (String role : p.roles())
if (grants.getOrDefault(role, Set.of()).contains(action)) return true;
return false; // no role granted it: deny by default
}
}

Two properties of this small class are worth saying to the interviewer. It denies by default, so a resource with no matching grant is refused rather than allowed, which is the safe direction for a system holding pay and personal data. And the grants themselves are per-tenant configuration instead of compiled-in constants. One customer's notion of a "Manager" role, and the actions it carries, differs from another's. That configurability of the permission model is the seam that lets the same codebase serve a bank and a hospital, and it is the natural bridge into how the domain objects are themselves configured.

Decision three: the business-object service

The core service owns the domain: it reads and writes the business objects the product is about, a worker, a position, an organization, a pay result. In a consumer app those would be fixed tables with fixed columns. In enterprise software they usually cannot be, because every tenant wants a slightly different shape, its own custom fields, its own required-versus-optional rules, its own object types layered on the standard ones. So the domain service is built around a model where the shape of an object is data the tenant configures. No developer hard-codes the columns. A tenant defines its field set; an object carries its values in a map keyed by those field definitions; the service validates a write against the tenant's model rather than against a rigid schema.

The second thing the domain object carries is time. A business record is more than a single current value. It's a timeline. This worker was grade three from January and grade four from June. If someone made a mistake, a correction to the June value must not erase the fact that grade three was in effect before it. So an object holds a series of versions ordered by the date each takes effect. Reading it means asking for the value in effect as of a given date: the latest version at or before that date. A TreeMap from effective date to version makes that read a single floor lookup. An amendment is just another entry, never an overwrite. Storing versions by effective date, instead of a mutable current row, turns a CRUD store into a system of record.

java
// A business object is a tenant-scoped record whose SHAPE is configuration, not
// code: each tenant defines its own fields, so values live in a map rather than
// fixed columns. History is kept BY EFFECTIVE DATE and never overwritten.
record FieldDef(String name, String type, boolean required) {}
record Version(LocalDate effectiveFrom, Map<String, Object> values) {}
class BusinessObject {
final String tenantId; // the isolation key, on every object
final String id; // stable identity across all versions
final String type; // "Worker", "Position", ... per tenant
// versions ordered by the date each takes effect
private final NavigableMap<LocalDate, Version> timeline = new TreeMap<>();
// The value in effect as of a date is the LATEST version at or before it.
Version asOf(LocalDate when) {
var entry = timeline.floorEntry(when);
return entry == null ? null : entry.getValue(); // null: before it existed
}
// An amendment is a new dated version, or a correction to an existing date.
// The prior value is retained, so the past stays reconstructable.
void amend(Version v) { timeline.put(v.effectiveFrom(), v); }
}

The two modeling choices pay off together. The field set is configuration, so onboarding a customer with an unusual data model is a configuration change. No schema migration or deploy is needed. The object is a timeline, so a follow-up like "what was this worker's grade last March" is a floor lookup. You never lost the value. Both live behind the domain service. The storage layer underneath can stay a plain set of tables while the richness sits in the model. That keeps the persistence decisions in the next section simple.

The architecture end to end

With the three core decisions made, the system is the wiring between them and the path of one request. Draw it. Read the diagram by role. Blue is an actor outside the system, a signed-in user or an admin driving a bulk integration. Violet is a service: the gateway that authenticates and scopes, the authorization service, the domain service, the workers, and the reporting service. Amber is the queue that carries events and jobs off the request path. Green is stored state. That covers the tenant metadata holding config and roles, the tenant-partitioned store of business objects, the append-only audit log, and the separate reporting store. Follow a write from the top. It gets authenticated and scoped, then checked for permission. It lands in the object store as a new dated version and is recorded in the audit log. Then it goes out as an event. The workers pick it up and roll it into the reporting store, where reports read.

Loading diagram...

Storage, consistency, and background work

Now the storage and consistency posture, which follows from what the data is. The business objects are money and law. Their store is a durable, strongly consistent transactional database, a relational store such as Postgres, partitioned by tenant. Strong consistency here earns its cost. A pay change or an approval has to read back correctly the instant after it commits. Two edits to the same record must not silently clobber each other. So writes are transactional, and optimistic version checks resolve concurrent edits instead of a last-writer-wins race. A consumer feed takes the opposite posture, where eventual consistency is a fine trade for scale. Explain why the trade flips for a system of record. The round looks for that reasoning.

Reporting pulls the other way, so give it its own store. A report over an entire tenant's population, like a headcount rollup or a compensation summary, is a heavy scan. Run against the same tables, it would contend with interactive writes. So the workers keep a separate reporting store, fed from the stream of change events, and reports read from it. That store may lag the write path a little. A summary tolerates being seconds stale in a way a pay calculation doesn't. Accepting that bounded staleness keeps a giant report from slowing down someone editing a record. The slow work runs on idempotent workers behind the queue: the payroll run, the rollups, the imports. The interactive write commits fast and emits an event, and the fan-out happens off the request path. Key the workers by the event. A worker that dies halfway through a payroll run and retries then recomputes instead of double-paying. In this domain, a duplicated side effect is a real person paid twice.

  • Object store: strong and transactional. Business objects are pay and legal state, so writes are transactional and read-your-writes consistent, with optimistic version checks to stop concurrent edits from clobbering each other. Eventual consistency, fine for a feed, is the wrong trade here.
  • Reporting store: separate, allowed to lag. Analytical scans read a store the workers keep fed from change events, away from the transactional tables. Heavy reports never contend with interactive writes. It tolerates bounded staleness because a summary can be seconds behind and a pay calculation cannot.
  • Background work: queue plus idempotent workers. The interactive write commits and emits an event; slow fan-out (a payroll run, a rollup, a bulk import) happens on workers behind a queue. Workers are keyed by the event so a crash-and-retry recomputes instead of double-paying a real person.
  • The audit log: append-only, never mutated. Every mutation appends who, what, and when-effective to an immutable log. It is written on the same path as the change so it cannot drift from reality, and never updated or deleted, so the trail itself is trustworthy under a compliance review.

Scale and failure handling

Scale in a multi-tenant system is a fairness problem before it's a throughput problem. Frame it that way. The load comes from thousands of tenants of wildly different sizes. The danger is a noisy neighbor. One large customer launches a huge bulk import or an all-population report and eats enough shared capacity to slow everyone else. The defenses are per-tenant. Rate-limit and quota work per tenant, so one company can't monopolize the request path. Cap the background work a single tenant can have in flight. A giant payroll run then drains at a bounded rate instead of swamping the worker pool. For the largest or most sensitive tenants, go back to the isolation decision from the first section. Give them dedicated capacity, such as a dedicated database or worker pool, so their load never shares a pipe with everyone else's.

Then walk the failure modes out loud. An enterprise buyer's real question is what happens on the bad day. A worker crashes mid payroll run. Every job is idempotent and keyed, so it resumes without paying anyone twice. A dependency is briefly down. Writes to the object store are transactional and either fully commit or fully roll back. No legal record is left half-changed. A bad deploy or a bad import corrupts a tenant's data. The isolation model has to allow a single-tenant restore without touching anyone else. That's a quiet argument for stronger isolation for premium accounts. The worst case is a query that escapes its tenant filter. Enforce the tenant at the database with row-level security, so an unscoped query returns nothing at all. Name these, and tie each to a design choice you already made. That shows you've run enterprise software as well as drawn it.

The Workday angle
Design so one tenant's bad day never drags the other tenants into it. That instinct reads as a Workday hire. Say the fairness controls out loud. Work is metered per tenant, so a bank running its month-end close can't slow a hospital on the same system. Background jobs are capped per tenant, so one payroll run drains at a bounded rate. The biggest accounts can move onto dedicated capacity, because the isolation model was a real decision from the start. Then go one step further and say how you'd prove the isolation holds. Write a test that acts as tenant A and asserts it can never read a single row belonging to tenant B. Run it against the real data layer, including the background jobs and the reporting path as well as the main API. Reason about the noisy neighbor and the cross-tenant leak, beyond the happy single-tenant demo. Enterprise software is built around that judgment.

Layering Workday's flavor

The framework so far is any multi-tenant enterprise SaaS. Now add what makes Workday its own product. The model was built to receive it, because each Workday distinctive is a decision you already made, turned up. Configurability is the business-object service built around a per-tenant field model instead of fixed columns. Push it further, and a tenant configures object types, validation rules, and business-process flows as well as fields, all as data. The same codebase then serves a bank and a university without a fork. Enterprise buyers care about that configuration-versus-customization line. The tenant configures within a model the vendor controls. Nobody forks code the vendor then can't upgrade.

Effective-dating is the timeline in the domain object, taken as seriously as a system of record for pay and headcount demands. A worker's compensation, position, and organization each carry their own dated history. A retroactive correction inserts a version without erasing what was in effect before. Reports can run "as of" any past date by asking each object for its value at that date. Audit and compliance are the append-only log, treated as product instead of plumbing. It records who changed what, when they changed it, and when it takes effect, immutable and queryable. A customer's auditors and regulators demand exactly that trail. Integrations are the bulk-admin actor and the queue, hardened into a first-class surface. No large enterprise runs one system. Inbound and outbound feeds to payroll providers, benefits vendors, and identity systems are a core requirement. Rate-limit them per tenant, so an integration burst never becomes a noisy neighbor.

On the multi-tenant framing itself. General prep will tell you Workday design interviews are all about tenant isolation and configurability. That describes what Workday builds, and it's useful context. The design round candidates report looks different. It's led by object-oriented, UML class-diagram modeling of an everyday domain (a ride-hailing app, a payments and cards system, a banking system). A lighter high-level round is often a deep dive on your own past project. So build this multi-tenant fluency as real domain knowledge, and lead with it if the prompt invites it. Don't walk in expecting a tenant-isolation battery. This design drills the skill the object-modeling round tests. Carve a domain into clean entities and relationships, then make them bend to a changing requirement without a rewrite.
The Workday angle
Tie every Workday distinctive back to a decision you already made. Then the design reads as one coherent system instead of a feature list. Configurability is the per-tenant field model, pushed until object types and business processes are data. The vendor can keep upgrading everyone on one codebase. Effective-dating is the object's timeline. A retroactive pay correction never erases the past, and a report can run as of any prior date. The audit trail is the append-only log, designed in from the start. A customer's auditors pay for the record of who changed what and when it took effect. Integrations are the bulk actor plus the queue, metered per tenant so a nightly feed never starves interactive users. Present those four as consequences of isolation, the domain model, storage, and background work. You've shown you can design the general system, then speak fluently about the enterprise one built on top.

The runnable core: a tenant-scoped effective-dated store

Interviewers often compress a whole design into one implementable class. For an enterprise system of record, the natural one is the store itself. It holds business objects for many tenants and keeps each object's history by effective date. It answers "what was the value as of this date" and never lets one tenant see another's data. It's the entire design in miniature. The two hardest invariants, tenant isolation and correct effective-dated reads, both live in that one class. There a candidate either states a clean rule or hand-waves.

The problem below strips the domain down to what runs and keeps the two invariants. Every operation is keyed by tenant first, so a lookup in one tenant can never surface another's key. A read as of a date returns the latest version at or before that date. A correction to an existing effective date overwrites that version's value and leaves the rest of the history intact. The exercise comes down to getting the tenant scoping and the as-of boundary exactly right, much as a min-stack comes down to the duplicate case.

1

Tenant-scoped effective-dated store: isolation and as-of reads

Medium
Problem
A system of record holds business objects for many tenants. Each object is identified by a key within its tenant, and its value changes over time. Every change carries an integer effectiveDate (think of it as a day number) from which that value is in effect. Reading an object means asking for the value in effect as of some date. Implement the store:

- RecordStore() creates an empty store.
- set(tenant, key, effectiveDate, value) records that key in tenant has value from effectiveDate on. If a version already exists at that exact effectiveDate, this is a correction: overwrite that version's value and leave every other version untouched. Returns the number of distinct versions stored for that (tenant, key) afterward.
- get(tenant, key, asOf) returns the value in effect as of asOf, meaning the value of the version with the greatest effectiveDate that is less than or equal to asOf. Returns null if the key does not exist in that tenant, or if asOf is before its first version.
- history(tenant, key) returns the full version history as a list of [effectiveDate, value] pairs sorted by effectiveDate ascending. Returns an empty list if the key does not exist in that tenant.
- keys(tenant) returns the keys held in that tenant, sorted ascending, and never any key from another tenant.

The exercise centers on tenant isolation and the as-of boundary. An operation in one tenant must never see another tenant's data. A read at a date exactly equal to a version's effectiveDate returns that version itself.

Input encoding. A test case is a JSON array of operations. The first entry is ["RecordStore", []]; each later entry is [method, [args]]. The expected array holds each operation's return, with null for the constructor.

Input: RecordStore(); set("acme","emp1",10,"grade-3"); set("acme","emp1",20,"grade-4"); get("acme","emp1",15); get("acme","emp1",5)

Output: set returns 1 then 2; get(...,15) returns "grade-3"; get(...,5) returns null

Two versions of emp1 exist: grade-3 from day 10 and grade-4 from day 20. As of day 15 the version in effect is grade-3 (10 is the greatest effective date at or before 15). As of day 5 no version has taken effect yet, so the answer is null.

Input: RecordStore(); set("acme","emp1",1,"a"); set("globex","emp1",1,"b"); get("acme","emp1",1); keys("acme")

Output: get returns "a"; keys("acme") returns ["emp1"]

Two tenants each have a key named emp1. The read in acme returns acme's value, never globex's, and acme's key list contains only its own key.

How this uses the pattern

Nest maps: tenant -> key -> (effectiveDate -> value). The set method is a three-branch map write, and a correction reuses the same effectiveDate slot. Get collects the effective dates at or before asOf and takes the value at the maximum. History sorts the inner map, and keys sorts the middle map. Tenant isolation needs no extra code, because you never look outside the requested tenant's sub-map.

Tenant-scoped effective-dated store: isolation and as-of reads

Loading...
Click "Run" to execute your code

Test Cases

Input: [["RecordStore",[]],["set",["acme","emp1",10,"grade-3"]],["set",["acme","emp1",20,"grade-4"]],["get",["acme","emp1",15]],["get",["acme","emp1",25]],["get",["acme","emp1",5]],["set",["acme","emp1",20,"grade-5"]],["get",["acme","emp1",25]]]Expected: [null,1,2,"grade-3","grade-4",null,2,"grade-5"]
Input: [["RecordStore",[]],["set",["acme","emp1",1,"a"]],["set",["globex","emp1",1,"b"]],["get",["acme","emp1",1]],["get",["globex","emp1",1]],["keys",["acme"]],["get",["globex","emp2",1]]]Expected: [null,1,1,"a","b",["emp1"],null]
Input: [["RecordStore",[]],["set",["acme","emp1",10,"g3"]],["set",["acme","emp1",5,"g2"]],["history",["acme","emp1"]],["get",["acme","emp1",7]]]Expected: [null,1,2,[[5,"g2"],[10,"g3"]],"g2"]
Input: [["RecordStore",[]],["set",["acme","zeta",1,"z"]],["set",["acme","alpha",1,"a"]],["keys",["acme"]],["keys",["ghost"]],["get",["acme","missing",5]]]Expected: [null,1,1,["alpha","zeta"],[],null]
Input: [["RecordStore",[]],["set",["acme","emp1",10,"g3"]],["set",["acme","emp1",20,"g4"]],["get",["acme","emp1",10]],["get",["acme","emp1",20]],["get",["acme","emp1",9]]]Expected: [null,1,2,"g3","g4",null]
The Workday angle
This is the enterprise system of record reduced to what runs. It drills the two invariants the full design lives on. Workday wants tenant isolation to be structural, never a filter you might forget. So the store is keyed by tenant first, and a lookup physically can't reach another tenant's data. Effective-dated reads must be exact too. A payroll report run as of a past date must return the value that was in effect then. The interviewer watches two cases survive the tests. At the as-of boundary, a read exactly at an effective date returns that version. In the correction case, a second set at an existing date overwrites the value without adding a version. Nesting two maps is the easy part.
Take it further
A natural Workday extension makes the object configurable and audited. First, values become a set of named fields defined per tenant, replacing the single scalar. So set validates the incoming fields against the tenant's model, and get returns the field map as of the date. Second, every set appends an immutable audit entry. It records who made the change, when they made it, and the effectiveDate it applies from. The history then holds the trail of who set the values as well as the values. Be ready to say what each costs. Validation turns set into a per-tenant model lookup. The audit log is an append-only list per key that get never reads, though a compliance query walks it in full.

What the round rewards

The bar on this design is a clean object model, honest isolation, and correct history. Scale math matters little. Whichever enterprise domain you get, the moves are the same. Frame the problem as a system of record before you draw anything, so you model the business instead of sizing a feed. Make the isolation decision first and name its cost, because it colors the schema and the blast radius of every bug. Put identity and role-based authorization in front of everything. Make the tenant match the outer gate, so a real role in one tenant can never reach another. Model the domain objects as configurable and effective-dated, so the shape bends per tenant and the past is never erased. Keep the write store strongly consistent. Push heavy reporting and slow fan-out onto their own store and idempotent workers. And design for the noisy neighbor and the cross-tenant leak as well as the clean demo. Do that and you read as someone who could own a piece of a real enterprise system. Then layer Workday's configurability, effective-dating, audit, and integrations on top. The same design carries the company flavor without a rewrite.

Key Takeaways

  • ✓Build the general multi-tenant SaaS first, then layer the company flavor. The decisions come in a fixed order because each constrains the next: tenant isolation, identity and role-based authorization, a configurable business-object service, storage and consistency, then background jobs and reporting. A candidate who only knows the Workday answer but cannot design this from scratch fails.
  • ✓Isolation is the invariant the whole system defends. Choose the model on purpose: a shared schema with a tenant key for many small tenants, a dedicated database for the few that demand it. Enforce it structurally, in the request context and at the database with row-level security. Then a forgotten filter can't leak one company's data into another.
  • ✓Model the domain as configurable and effective-dated, which is what turns a CRUD store into a system of record. Object shape is per-tenant configuration instead of fixed columns. Each record is a timeline of versions keyed by effective date. A read is a floor lookup as of a date, and a correction never erases the past.
  • ✓Keep the write store strongly consistent, because it holds pay and legal state. Push reporting to a separate store that is allowed to lag. Run slow fan-out on idempotent workers behind a queue, so a crash-and-retry never double-pays. Defend the bad day: meter work per tenant against the noisy neighbor, and prove no tenant can read another's rows.
  • ✓The runnable core is the store itself, a tenant-scoped effective-dated map where isolation is structural and the as-of read is an inclusive floor lookup. Workday's distinctives, configurability, effective-dating, audit, and integrations, are each one of these decisions turned up, so the same design speaks the enterprise language without a rewrite.
Had your Workday interview?Tell us what they asked. Reviewed before it goes in, never with your name.
Workday interview guide

Know what Workday asks before you walk in.

10 coding · 3 system design · 15 behavioral questions

Get Premium$49 a year · Cancel anytime
Or start freeNo card.

Get their questions first.

InterviewLegend is independent and not affiliated with or endorsed by any company it covers.