Inside Feast's Technical Architecture
A detailed look at Feast's architecture and core feature-store capabilities.
Translated from Chinese with AI · Read the original
Feast and Tecton are well-known feature-store systems and of interest to many feature engineers. This article examines several core Feast capabilities.
Product Positioning
Feast (Feature Store) is a customizable operational data system that re-uses existing infrastructure to manage and serve machine learning features to realtime models.

Feast positions itself as a data system with three main responsibilities:
- Store features
- Serve features
- Register feature metadata
These support capabilities such as streaming ingestion, consistent batch/stream storage, and feature lineage.
Concepts

- Project: An isolated project, similar to a tenant or namespace.
- Feature View: A project contains multiple feature groups. TTL determines the allowed historical lookup range, and lightweight on-demand transformations are supported.
- Feature: One feature.
- Entity: The primary key associating a set of features with an entity ID.
- Data Source: The source underlying a feature view.
Example
In an Uber-like use case, drivers and customers are two entities. A driver’s data source maps to a feature view containing driver-profile features, with entity driver and primary key driver_id. A separate customer feature view uses customer_id and holds customer features.
Architecture

Feast Apply initializes the registry and metadata. The SDK then operates on offline and online stores. New data is written to the offline store through Spark or SQL and copied into the online store through Materialize.
Data Source
Data sources include:
- Batch DataSource: Warehouse data.
- Stream DataSource:
- Push Source: Lets users push features into Feast’s offline and online stores.
- Stream Source: Connects to Kafka or Kinesis.
- Request DataSource: Supplies data only when a request is made, for OnDemandFeatureView.
Batch and stream sources are straightforward. Push Source exposes an API for writing features directly into online and offline stores, avoiding a separate materialization step. The following focuses on Push Source and RequestDataSource.
Request DataSource currently provides relatively basic UDF support, used as follows:
def udf2(features_df: pd.DataFrame) -> pd.DataFrame: df = pd.DataFrame() df["output1"] = features_df["feature1"] + 100 df["output2"] = features_df["feature2"] + 100 return df
on_demand_feature_view_3 = OnDemandFeatureView( name="my-on-demand-feature-view", sources=sources, schema=[ Field(name="output1", dtype=Float32), Field(name="output2", dtype=Float32), ], udf=udf2, udf_string="udf2 source code",)The UDF operates on a pandas DataFrame. For Hive or other big-data sources, each engine’s _to_df_internal first converts data into a single-machine pd.DataFrame, then applies the UDF. Yes, datasets too large for one machine cannot be handled this way.
Offline Store with Time Travel
Offline stores support BigQuery, files, Redshift, Snowflake, Athena, MS SQL, Postgres, Spark, Trino, and other common systems.
The important get_historical_features(entity_df, features) interface retrieves the correct feature value for an entity at a timestamp. To avoid future-data leakage, explained here, it finds the nearest historical value without going past that time. This is a point-in-time join.
entity_df supplies entity IDs, such as driver_id, and timestamps; features names the requested features. Registry metadata locates their feature views and sources, then each source performs its point-in-time join. Consider Spark:
Spark implements the flow efficiently in Spark SQL using templates. Two statements are central:
{{ featureview.name }}__base AS ( SELECT subquery.*, entity_dataframe.entity_timestamp, entity_dataframe.{{featureview.name}}__entity_row_unique_id FROM {{ featureview.name }}__subquery AS subquery INNER JOIN {{ featureview.name }}__entity_dataframe AS entity_dataframe ON TRUE AND subquery.event_timestamp <= entity_dataframe.entity_timestamp
{% if featureview.ttl == 0 %}{% else %} AND subquery.event_timestamp >= entity_dataframe.entity_timestamp - {{ featureview.ttl }} * interval '1' second {% endif %}
{% for entity in featureview.entities %} AND subquery.{{ entity }} = entity_dataframe.{{ entity }} {% endfor %} ),subquery contains feature-view data after coarse timestamp filtering. entity_dataframe is the supplied entity_df with IDs and requested timestamps. The join conditions are:
- Feature-view timestamp < requested timestamp.
- Feature-view timestamp > requested timestamp - TTL.
- Matching entity IDs.
This retains historical values for matching entities. Next, select the feature value nearest the requested timestamp:
{{ featureview.name }}__latest AS ( SELECT event_timestamp, {% if featureview.created_timestamp_column %}created_timestamp,{% endif %} {{featureview.name}}__entity_row_unique_id FROM ( SELECT *, ROW_NUMBER() OVER( PARTITION BY {{featureview.name}}__entity_row_unique_id ORDER BY event_timestamp DESC{% if featureview.created_timestamp_column %},created_timestamp DESC{% endif %} ) AS row_number FROM {{ featureview.name }}__base {% if featureview.created_timestamp_column %} INNER JOIN {{ featureview.name }}__dedup USING ({{featureview.name}}__entity_row_unique_id, event_timestamp, created_timestamp) {% endif %} ) WHERE row_number = 1 )A standard ROW_NUMBER window orders event_timestamp descending and selects row_number=1. Together, these operations retrieve the nearest historical feature value for every entity.
Online Store
Online stores support common low-latency systems such as Bigtable, Cassandra, DynamoDB, HBase, MySQL, and Redis.
Materialization copies each entity’s latest offline feature values into the online store. The materialize_incremental method expresses this operation:
For Spark, identify feature views present in both stores, derive start_date and end_date from the last synchronization time, and invoke the engine’s materialize implementation. Again, Spark uses SQL:
SELECT {field_string} {f", {repr(DUMMY_ENTITY_VAL)} AS {DUMMY_ENTITY_ID}" if not join_key_columns else ""}FROM ( SELECT {field_string}, ROW_NUMBER() OVER({partition_by_join_key_string} ORDER BY {timestamp_desc_string}) AS feast_row_ FROM {from_expression} t1 WHERE {timestamp_field} BETWEEN TIMESTAMP('{start_date_str}') AND TIMESTAMP('{end_date_str}')) t2WHERE feast_row_ = 1field_string represents feature fields. ROW_NUMBER() selects the latest values per entity by timestamp. The resulting Spark DataFrame is written to the online store in parallel through foreachPartition:
spark_df.foreachPartition(lambda x: _process_by_partition(x, spark_serialized_artifacts))Registry
The registry stores metadata. After materialization, for example, it records each feature view’s latest materialization time. It contains entities, feature_views, data_sources, feature_services, and related concepts. It can be thought of as a protobuf object, which is also how it is implemented.
Two registry forms are supported:
- Local/remote: Cache a RegistryProto object locally, then synchronize changes to remote storage such as S3 or GCS.
- SQL registry: Access and update metadata through databases such as Postgres or SQLite.
Impressions
Open-source Feast contains the essential components in a simple, understandable architecture. With some extensions, it should meet many companies’ early needs. Its strengths include:
- Friendly APIs and a good experience for algorithm engineers and data scientists, though not necessarily Feast developers. Most impressive, users need not distinguish online from offline when defining features: names, computation, semantics, and values align across both, avoiding a separate consistency burden.
- A broad ecosystem, including many AWS products, helps cloud users build quickly without reinventing integrations. Much of the code is ecosystem support, which likely attracts users even while core/runtime capabilities remain limited.
- Concise interfaces and consistent abstractions. Consider this demo workflow:
def run_demo(): store = FeatureStore(repo_path=".") print("\n--- Run feast apply ---") subprocess.run(["feast", "apply"])
print("\n--- Historical features for training ---") fetch_historical_features_entity_df(store, for_batch_scoring=False)
print("\n--- Historical features for batch scoring ---") fetch_historical_features_entity_df(store, for_batch_scoring=True)
print("\n--- Load features into online store ---") store.materialize_incremental(end_date=datetime.now())
print("\n--- Online features ---") fetch_online_features(store)
print("\n--- Online features retrieved (instead) through a feature service---") fetch_online_features(store, source="feature_service")
print( "\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---" ) fetch_online_features(store, source="push")
print("\n--- Simulate a stream event ingestion of the hourly stats df ---") event_df = pd.DataFrame.from_dict( { "driver_id": [1001], "event_timestamp": [ datetime.now(), ], "created": [ datetime.now(), ], "conv_rate": [1.0], "acc_rate": [1.0], "avg_daily_trips": [1000], } ) print(event_df) store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE_AND_OFFLINE)
print("\n--- Online features again with updated values from a stream push---") fetch_online_features(store, source="push")
print("\n--- Run feast teardown ---") subprocess.run(["feast", "teardown"])Just over thirty lines retrieve offline point-in-time features and online features, and write new features in streaming and batch modes. Different stores do not introduce inconsistent interfaces or excessive parameters. One Python feature_store object handles all feature operations, even within one file.
Two weaknesses:
- Limited data scale: Even Spark workflows convert DataFrames to pandas for UDFs and other operations, demanding substantial single-machine capacity.
- The architecture remains mainly offline-oriented, with features entering the offline store and being synchronized online. Real-time feature support is still limited.