
Feature Stores Without a Platform Team: A Pragmatic Blueprint
You don't need Tecton or Feast on day one. Here's how to get 80% of a feature store's benefits with dbt, Postgres, and a Redis cache — and when to graduate.
A real feature store solves two problems: training/serving skew and reusing features across models. You can solve both with dbt for the offline layer, Postgres or Redis for the online layer, and a thin Python wrapper — no platform team required. Adopt Feast or Tecton only when the pain justifies the tax.
- Training/serving skew is the top ML bug — features computed differently at train time and serve time.
- dbt models are your offline feature definitions; use them to populate the online store.
- The online store can be Postgres for <10ms lookups at moderate scale; Redis when you need <2ms.
- A tiny Python client with a shared feature registry prevents feature drift across teams.
- Adopt a full feature store when 3+ teams share features or you need point-in-time correctness at scale.
The problem, minus the marketing
A feature store exists because ML models fail in two predictable ways:
- Training/serving skew. At training you compute 'orders in last 30 days' via SQL. At serving, the app calls a Python function. Two implementations, subtle differences, model in production performs worse than in the notebook.
- Feature duplication. Team A builds a 'customer LTV' feature. Team B builds a slightly different one. Team C builds a third. Now there are three definitions of the same thing.
Everything a feature store does is in service of these two problems. You can solve both with tools you already have.
Offline features live in dbt
Every batch feature is a dbt model. One SQL definition, one source of truth, materialized to your warehouse for training, exported to the online store for serving:
-- models/features/customer_features.sql
select
customer_id,
count(*) filter (where order_date > current_date - 30) as orders_30d,
sum(total_cents) filter (where order_date > current_date - 90) as gmv_90d,
max(order_date) as last_order_at,
current_timestamp as computed_at
from {{ ref('stg_orders') }}
group by 1This model is your feature contract. Any team using orders_30d reads it from this table. Change the definition, everyone sees it. Version via dbt's -- @deprecated comment or renaming.
Online store — Postgres first
For serving latencies of 10-50ms and QPS below a few thousand, a Postgres table with a simple sync job is enough:
create table online_customer_features (
customer_id uuid primary key,
orders_30d int,
gmv_90d bigint,
last_order_at timestamptz,
computed_at timestamptz
);
-- nightly sync from warehouse
insert into online_customer_features
select * from warehouse.customer_features
on conflict (customer_id) do update set
orders_30d = excluded.orders_30d,
gmv_90d = excluded.gmv_90d,
last_order_at = excluded.last_order_at,
computed_at = excluded.computed_at
where excluded.computed_at > online_customer_features.computed_at;When p99 lookup latency needs to be <5ms, add Redis as a read-through cache in front. Do not start with Redis — you'll spend weeks on cache invalidation for a problem you don't yet have.
The shared client
Ship a tiny Python package that every model consumes. It hides the storage and enforces the registry:
class FeatureClient:
def get(self, entity: str, entity_id: str, features: list[str]) -> dict:
table = REGISTRY[entity].table
cols = ','.join(features)
return db.fetch_one(f'select {cols} from {table} where {entity}_id = %s', entity_id)
REGISTRY = {
'customer': FeatureSet('online_customer_features', ['orders_30d', 'gmv_90d', 'last_order_at']),
'product': FeatureSet('online_product_features', ['view_count_7d', 'purchase_count_7d']),
}Any code that asks for a feature not in the registry raises. Any dbt model that produces a feature registers it via a macro. Two-way enforcement, zero platform team.
Training/serving skew, solved
Training reads from the warehouse table. Serving reads from the online table populated by exporting the same warehouse table. There is one implementation of every feature — the dbt model — and it runs once per day. Skew becomes structurally impossible for batch features.
For real-time features (last click, current cart) you still need to be careful — a separate section. But for the 90% that are 'aggregate over the last N days', this pattern eliminates the class of bug entirely.
When to graduate to Feast, Tecton, or Vertex Feature Store
- You have 3+ teams sharing features and the ad-hoc registry becomes a coordination bottleneck.
- You need point-in-time correctness for training — 'give me the value each feature had at the moment of each historical prediction' is genuinely hard to do in dbt alone.
- You need streaming features (last 60 seconds of activity) with sub-second freshness.
- You need feature lineage, monitoring, and drift detection out of the box.
Feast is the cheapest step up — open source, works with Postgres/Redis, no cloud lock-in. Tecton and Vertex Feature Store are managed but expensive; only adopt when the head count you save exceeds the license cost, which is usually at a 20+ ML engineer scale.
Frequently asked
Isn't this a feature store already?
In everything but marketing terms — yes. Which is the point.
What about training-time joins?
For basic training sets, join the feature table to your labels in SQL. For point-in-time correctness, use dbt's snapshots or a lightweight event log; only migrate to a real store when this hurts.
Does this work for real-time features?
Partially. Batch features (last 7d aggregates) are perfect. For truly real-time features (last 60s clicks), you'll need a streaming layer — Kafka + a materialized store — but you can add that just for the 2-3 features that need it.
Building something similar?
I help teams ship production-grade AI agents, n8n workflows, and data platforms. Let's talk about what you're building.
Work with me
Islam Gamal
AI, Data & Automation Engineer. I design and ship production AI agents, n8n workflows, and cloud data platforms — with a focus on reliability, cost, and measurable business impact. Founder of Tashghil and Tek bil Arabi.
More from Islam Gamal
MLOps on a Shoestring: A Complete Stack for Under $200/Month
The pragmatic MLOps stack for solo builders and small teams — model registry, experiment tracking, feature store, drift monitoring, and CI/CD — all under $200/month with open-source and free tiers.
The Modern Data Stack in 2026: What Actually Matters (and What to Skip)
A no-hype 30-minute tour of the 2026 data stack: ingestion, warehouse, transformation, orchestration, semantic layer, reverse ETL, observability, and the AI layer on top — with concrete tool picks, cost anchors, and the anti-patterns that quietly eat budgets.
dbt in Anger: Modeling, Contracts, Testing, CI, and the Setup That Scales to 500+ Models
The dbt setup that survives real teams — layered models, contracts on every mart, tests that catch business bugs, CI that runs only what changed, semantic layer for one metric one place, and the ops habits that keep the graph healthy at 500+ models.
Browse every article by Islam Gamal on the author page.