What we would tell a team starting today.
By: Praveen Sundaresan Ramesh and Gayathri Lekshmi Sadasivan.

Introduction
A large enterprise seldom runs a single Machine Learning (ML) model. It runs dozens, built by different teams across different workspaces. Many of these models depend on the same underlying data and calculation logic. For instance, a fraud detection model, a customer churn model, and a client lifetime value model might each need card_txn_count_30d, the number of transactions incurred by a card in the last thirty days. Without a shared space, the feature gets rebuilt three times and sometimes in three slightly different ways.
Production ML pipelines in an enterprise typically involve a robust MLOps practice, and one of its foundational steps is data preparation and feature engineering. It is the process of transforming and aggregating data from curated silver and/or gold tables into refined columns, called features, that ML models consume. It might be tempting to treat this as a one-time exercise that ends when training ends. Unless the transformation logic is tracked and governed, the features computed at inference could drift from the ones computed during training. This could either be the transformation code that diverged between the training and inference pipeline, or the values used at training time were never actually available when the label was recorded. Both are training/serving skew that degrade models in production.
What you want instead is a single place to define, discover, govern, and reuse features, backed by a consistent transformation pipeline. Databricks’ solution for this is the feature store, built on unity catalog (UC). We are hoping for this article to be more like a field report rather than a paraphrase of the documentation. We’ll start with a brief introduction of feature store, talk about the different ways to author features, and share our lessons learned along the way of implementation.
What is a Databricks feature store?
In Databricks, a feature store is a central registry that manages the definition, computation, storage, and serving of machine learning features as governed objects.
Built into the feature store are capabilities of
- governance and lineage,
- point-in-time joins,
- automatic feature lookups,
- on-demand features using UDFs,
- cross workspace feature discovery and sharing.
Feature table vs feature view
Databricks provides two pathways to authoring features: Feature tables and Feature Views.
Feature tables were the go-to standard up until July 2026, when Databricks announced the public preview of feature views. The key difference between the two being who owns the pipeline that populates and refreshes the table. With feature tables you own the pipeline, while with feature views you declare the feature as a native UC object and Databricks builds and manages the pipeline for you.
A feature table is a Delta table in UC with a primary key. Any UC Delta table with a primary key constraint can act as a feature table, which means your existing gold tables, streaming tables, and materialized tables are candidates today. There is no proprietary format nor any imports or exports. This materially lowers the risk of starting with feature tables and the adoption is reversible.
Note: Unity Catalog primary key constraints are informational only and are not enforced. The feature engineering client will reject duplicate primary keys at the point it creates a table for you, but a primary key constraint that you add to an existing gold table is not enforced. If you are using an existing Delta table into the feature store, you have to enforce uniqueness assertion to the pipeline.
Feature views on the other hand is a simple abstraction where you define features declaratively. Based on the defined feature logic, Databricks’ feature store will generate point-in-time accurate data for training. Once it is ready to be served, you can materialize the features to offline and online stores and Databricks will run the pipelines to refresh feature values for inference. The same definition can be used for both batch and streaming sources.
| Feature Table | Feature View | |
| What the object is | A UC delta table or streaming table with a primary key | A native UC Feature object holding the feature definition |
| Who owns the pipeline | You. You write feature values to the table and own the pipeline that populates and refreshes it | Databricks. You can materialize features and the platform creates and runs serverless Lakeflow pipelines |
| Feature logic | Flexible. Arbitrary Spark, SQL, and multi-stage pipelines. You can compute anything | Declarative and constrained. Fixed aggregation functions plus column selection. Row wise expressions are also supported |
| Historical values for training | Read from the feature table you maintain, using table’s point-in-time semantics where applicable | Point-in-time correct values are computed on-demand from source data. Where offline materialization exists, precomputed data is used instead |
| Online serving | You maintain and publish an offline table to be synced online with three publish modes (triggered, snapshot, continuous) | Managed materialization. Column selection features and streaming features are online only. Aggregation features for online also requires an offline store |
| Streaming freshness | Possible, but you build and operate the streaming path | First class managed streaming materialization with preview constraints |
| Governance and lineage | Standard UC governance plus feature-table metadata | Feature-specific UC governance. Feature objects come with CREATE FEATURE, READ FEATURE, and MANAGE permissions |
| Maturity | Generally available | Public preview |
When it comes to billing, there are three components that carry a charge and all three are optional:
- Databricks managed feature materialization (only if you’re using feature views),
- Lakebase or another supported online store (only if you have real-time inference),
- feature serving endpoints (typically for models hosted outside of Databricks).
Consequently, for a batch-only use case, feature store adds very little separate infrastructure and in return you get point-in-time feature lookup, lineage, discovery, and UC governance.
Lessons learnt
Lesson 1: Design the catalog by entity key and refresh cadence, not by feature family
A common instinct is to organize features into tables by business domain: transaction velocity features in one table, spending features in another, demographics in a third. That can be useful for organizing and discovering features, but this is not the right fit for a feature table. Instead, you start by asking two questions:
- What entity does the feature describe?
- How fresh does the feature need to be?
The first question is not optional. In UC, any delta table with a primary key constraint can serve as a feature table as discussed earlier, and feature lookups resolve on that key. Entity therefore is crucial in designing the catalog.
Table 2 shows a sample set of features for a real-time fraud detection system. It is tempting to group all spending features together, but avg_transaction_amount_30d for example refreshes daily while transaction_amount_last_15min refreshes continuously. Grouping them, the entire table would need to be refreshed continuously. That would require running a Lakeflow pipeline in continuous mode to recompute a number that barely changes daily. That would also propagate downstream. Publishing that table to the online store re-syncs the slow columns on every fast update.
| Feature | Entity | Cadence | Domain |
avg_transaction_amount_30d | customer_id | daily | Spending |
transaction_count_90d | customer_id | daily | Velocity |
customer_risk_score | customer_id | daily | Risk |
transaction_count_last_5min | customer_id | streaming | Velocity |
transaction_amount_last_15min | customer_id | streaming | Spending |
merchant_decline_count_last_10min | merchant_id | streaming | Risk |
merchant_avg_transaction_amount_1h | merchant_id | hourly | Spending |
merchant_transaction_count_1h | merchant_id | hourly | Velocity |
merchant_risk_score_1h | merchant_id | hourly | Risk |
customer_income_band | customer_id | source-triggered | Demographics |
merchant_zipcode | merchant_id | source-triggered | Demographics |
This translates to both feature tables and feature views. For feature views, group features into shared materialization pipelines. Features that share an offline and online destination, and trigger can materialize together to reduce the number of pipelines.
What we recommend:
- Draw the physical boundary on entity key first then on refresh cadence.
- When a feature’s freshness is unclear, default to a slower one.
- Leverage UC tags and comments to preserve the domain that people actually search and discover features by.
- Name the objects based on what distinguishes them. Example
customer_features_daily,customer_features_streaming
Lesson 2: Do not assume online serving reproduces the semantics you trained on
Point-in-time semantics apply to both training and Databricks batch inference. Online inference is different; it retrieves the current feature value available in the online store.
| Path | Semantics | Behaviour |
| Training | AS-OF join | For a time-series feature table, Databricks retrieves the latest feature value at or before the lookup timestamp. |
| Batch Inference | AS-OF join | score_batch uses the feature metadata stored with the logged model to perform point-in-time feature lookup. |
| Online Inference | Latest value | Online feature lookup does not perform an AS-OF join; the serving path uses feature value currently available in the online store. |
Point-in-time correctness depends on declaring the temporal semantics correctly. Databricks does not infer that a timestamp column should be treated as a temporal lookup just because its type is TIMESTAMP or DATE. A time-series feature table must be created or declared appropriately, which in practice means passing timeseries_columns at creation time.
Databricks enforces the point-in-time semantics that you configure, it does not validate if your configuration correctly depicts the real-world availability of the feature. For example, a feature might have an event_time that says when a transaction occurred, while the model should really use the time at which that transaction became observable in the production system.
Databricks documents that online store supports primary-key lookup rather than point-in-time lookup. That creates a potential mismatch between how the model was trained and how it is used online. During training, the model sees the feature value that was available as of the historical training timestamp. During online inference, the model receives the latest feature value available in the online store when the request is served.
Those values are not necessarily the same. If a feature changes between the historical training timestamp and the moment of online inference, the model may receive a value with different freshness or distribution characteristics from the values it saw during training.
Likewise, lookback_window on feature lookup excludes feature values older than the specified window. It is applied during training and batch inference and is ignored during online inference, where the latest value is used regardless. This is similar to the asymmetry described above, but more pronounced, as you have explicitly declared a staleness tolerance that online serving does not honour.
What we recommend for models served online:
- Design the training timestamp around the production feature cadence. If a feature is operationally refreshed hourly, an hourly training grid may produce a more representative approximation of what the model sees online.
- Use continuously updated/streaming features when the requirement is low-latency freshness. Databricks supports streaming features specifically for cases where feature values need to update continuously. That can reduce freshness lag substantially, but it does not change the fundamental distinction between current online lookup and offline AS-OF lookup.
- Monitor production feature distributions. Compare what is served with the distributions observed during training and validation.
- Treat feature age as an explicit modeling and monitoring concept. A new feature such as
inference_time – feature_timestampcan expose how stale the input is and, where appropriate, let the model learn that staleness matters.
Lesson 3: On-demand features require special attention
Some features cannot be precomputed. The distance between a transaction’s location and the customer’s home address, for example, depends on the transaction location that is only available at the request time. These become on-demand features, where regular Python functions registered in UC can be used to evaluate them inside the serving container at request time.
Note: The model must be logged using fe.log_model to ensure that the model automatically computes on-demand features at inference time.
They are extremely useful for real-time use cases, but special attention is required for the following:
- Missing value semantics differ between batch and online. You need to handle nulls, because the platform will not. When a feature lookup returns no value, batch inference returns
None, while online inference returnsfloat(“nan”).
- A model can use up to 100 on-demand features. Separately, UC allows at most five UDF calls per query, a spark query plan limit that raises
UDF_MAX_COUNT_EXCEEDED. It counts UDF instances in the plan and not the distinct functions.
- There is a one-to-one mapping between UDF, and features and the output types are restricted.
FeatureFunctionbinds one UDF to one output name, and UC UDFs return a scalar. For feature serving endpoints, there are restrictions on the output data types.ArrayType,MapType, andStructTypeare unsupported as output types.
What we recommend:
- Guard every UDF against both
NoneandNaNand make it a lint rule that fails the build. In addition, set explicit default values on lookups while writingFeatureLookup, so the function ideally never sees a missing value.
- Declare every package a UDF imports in both the UDF’s
ENVIRONMENTclause andextra_pip_requirementsonfe.log_model.
- If more than five on-demand features are required, raise
spark.databricks.safespark.externalUDF.plan.limiton the training and batch-scoring clusters only, alongside the memory-tracking settings Databricks lists with it. Keep in mind, this introduces the OOM risk the cap exists to prevent.
Lesson 4: Two things bill continuously and neither stops on its own
Databricks recommends Lakebase as the online store for feature tables, and feature views materialize online only through Lakebase. Although, Lakebase offers scale-to-zero option, the online feature store does not support it. An online store therefore continuously incurs cost, and it adds up for every duplicated store in development and test environments.
CONTINUOUS publish mode is the second always-on object: a streaming pipeline per online table, running continuously whether the data underneath is refreshed or not. Databricks offers three publish modes as shown in Table 4.
| Mode | What it does | Cost profile |
TRIGGERED (default) | Incremental updates, on demand or on a schedule | Good cost/lag balance; expensive below 5-minute intervals |
SNAPSHOT | Full copy on every sync; schedulable | ~10x more efficient when >10% of rows change per cycle |
CONTINUOUS | Streaming pipeline, seconds of latency | Lowest lag, highest cost; 15-second minimum |
Both TRIGGERED and CONTINUOUS require change data feed (CDF) enabled on the source table, which adds write amplification to every merge into the feature table.
What we recommend:
- Consolidate online stores. For development, testing, and production environments, Databricks recommends sharing one store across projects and users rather than creating separate stores.
- Match publish mode to cadence, and do not resort to a blanket default. Tag each feature table with
update_frequencyand derive the mode from it. ReserveCONTINUOUSmode for features whose staleness the model cannot tolerate.
- Create a purpose-built feature table or a simple select view containing only the columns that the endpoint consumes and publish that. Synced tables accept views and materialized views as sources.
- Delete unused online stores and their tables in non-production environments.
- When one feature table is published to multiple online stores, serving endpoints resolve to the oldest by creation timestamp.
Lesson 5: Feature views are the future of the platform, and a preview surfaces today
Databricks’ recommendation is to use feature views for most new use cases. While the simplicity and declarative nature of feature views are tempting, there are some limitations that we want to flag.
- A feature computes exactly one aggregation, and nothing runs after it. The flow is
filter_condition(a row-wiseWHEREapplied to the data source) ->transformation_sql(a row-wiseSELECTprojection) -> one aggregation operator over one window. When both are defined, the resolved query is SELECT {transformation_sql} FROM {table} WHERE {filter_condition}. Only row-wise expressions such as renames, casts are accepted. Null handling and derived columns must be addressed before aggregation. Post-aggregation arithmetic is out of scope. You cannot express a ratio of two aggregated features, or coalesce an aggregate result, inside a feature view.
- The aggregation operator set is fixed and two of them are approximations:
Sum · Avg · Count · Min · Max · First · Last · StddevPop · StddevSamp · VarPop · VarSamp · ApproxCountDistinct · ApproxPercentile · FirstN · LastN · FirstDistinct · LastDistinct.
The feature views API documents no support for mode, exact distinct counts, median, correlation, or user-defined aggregations. The approximations are deterministic and do not cause any skew, but it does produce slightly different numbers from what is expected.
- Feature views and on-demand functions cannot be served together. A
FeatureSpecholding feature views cannot holdFeatureLookuporFeatureFunctionentries. So on-demand functions like the distance between a transaction and the cardholder’s home address cannot be computed for a model whose other features are feature views.
- Batch aggregation features can materialize offline and online, although batch rolling-window features cannot be materialized. Column-selection and streaming features can materialize to online stores only. Request-source features cannot be materialized at all, and that’s by design. Because streaming features do not have offline materialization, their values are recomputed from the source data during training.
- Streaming Feature View materialization does not backfill historical records. Materialization begins with records that arrive after the pipeline starts, so a newly materialized rolling-window feature is incomplete until a full window of data has accumulated. For example, a newly started 30-day rolling-window stream will not contain a complete 30-day history until 30 days of data have been processed.
- Earlier we learned that online inference does not perform point-in-time lookup, it returns the latest value available for a primary key. The refresh cadence controls when a feature view is materialized, it does not create new observations for entities that have no source records. If no new data arrives, the online store continues to serve stale value. With feature tables, you can work around this by building a spine pipeline that writes rows at regular intervals, ensuring the feature computation is evaluated for the entity even when no new event has occurred.
What we recommend:
If any of these limitations apply to your use case, stick to feature tables. Keep feature views as an experimentation layer until the preview constraints are lifted and keep monitoring Databricks’ documentation for latest updates.
