An issue-tracking product lets every customer define custom fields, workflows and screens. Which data model should the platform use for custom fields?
Show the full answer Hide the answer
Why the pure options fail
Entity-attribute-value. One row per field value is maximally flexible and pathologically slow. Retrieving one issue with 30 custom fields becomes 30 rows to gather; filtering on two custom fields becomes a self-join; sorting on a custom field becomes an aggregation. Types are lost, so validation moves to application code and the database can no longer help. EAV is the classic answer that looks right in a design review and collapses under a query planner.
Table per tenant. Excellent isolation and query performance, catastrophic operational cost. Schema migrations must run across every tenant's tables, so a change that takes seconds for one customer takes days across the estate, with partial-failure states. The catalogue itself becomes enormous, and connection pooling and query-plan caching degrade. It is viable for tens of tenants and not for thousands.
Wide table with generic columns (custom_string_1 … custom_string_50). It performs adequately and
is a semantic disaster: the meaning of a column varies per tenant, nothing is self-describing, the
limit is arbitrary and eventually hit, and every query needs a mapping layer.
Why the hybrid wins
Split the model by who owns the schema:
- Platform-owned concepts get real columns. Identifier, tenant, project, status, assignee, timestamps. These exist for every tenant, are queried by every feature, and benefit from constraints, foreign keys and conventional indexes.
- Tenant-defined fields go in a semi-structured column — a JSON document with a per-tenant field definition table describing types, validation and display. One row per issue, retrieved in one read.
- Targeted indexes on the document for the field paths that are actually filtered and sorted, which is a small subset. Modern relational engines index inside JSON documents well.
- A separate search index for the general case: full-text, complex filters and cross-field queries go to a search engine fed asynchronously, not to the transactional store.
The design principle
Put the boundary where schema ownership changes. Anything the platform defines gets platform structure. Anything a tenant defines gets a flexible container with tenant-supplied metadata. Mixing those two ownership models into one mechanism is what produces both EAV and the wide-table disaster.
The trade-off to state explicitly
You accept weaker database-level guarantees on the flexible half — the engine cannot enforce that a tenant's "due date" is a date, so validation lives in application code and must be applied consistently by every write path, including imports and integrations. That is a real, permanent tax, and it is the price of the flexibility the product sells.