Building Our ML Framework
Why we built a custom ML framework and what we learned along the way.
When we started applying ML to commerce, we used standard frameworks. As scale grew, limitations emerged. This is the story of building our own ML framework.
The Problem
By 2021, Hanzo ran dozens of ML models:
- Recommendation engines
- Fraud detection
- Demand forecasting
- Price optimization
- Search ranking
- Churn prediction
Each model had its own:
- Training pipeline
- Serving infrastructure
- Monitoring setup
- Feature engineering code
Duplication was enormous. Changes were slow. Bugs were common.
Requirements
We needed a framework that:
- Unified training and serving: Same code for both
- Feature reuse: Define once, use everywhere
- Incremental learning: Update models without full retraining
- Low latency serving: Sub-10ms inference
- Automatic monitoring: Drift detection, performance tracking
- Commerce primitives: First-class support for our domain
No existing framework met all requirements.
Architecture
Feature Store
Central repository for all features:
from hanzo.ml import FeatureStore, Feature
store = FeatureStore()
# Define feature
@store.feature
def customer_lifetime_value(customer_id: str) -> float:
orders = get_orders(customer_id)
return sum(o.total for o in orders)
# Use in any model
features = store.get_features(
customer_id="cust_123",
features=["customer_lifetime_value", "days_since_last_order"]
)
Features computed once, cached, and served to all models.
Model Registry
Version-controlled model storage:
from hanzo.ml import Model, Registry
registry = Registry()
# Register model
model = train_model(data)
registry.register(
model=model,
name="churn_prediction",
version="1.2.3",
metadata={"accuracy": 0.89, "training_data": "2022-07-01"}
)
# Load model
model = registry.load("churn_prediction", version="latest")
Training Pipeline
Declarative training configuration:
name: churn_prediction
version: 1.2.3
data:
source: customers
features:
- customer_lifetime_value
- days_since_last_order
- support_tickets_30d
label: churned_90d
model:
type: gradient_boosting
hyperparameters:
n_estimators: 100
max_depth: 6
training:
split: 0.8
validation: time_based
evaluation:
metrics: [auc, precision, recall]
threshold: { auc: 0.85 }
Run training:
hanzo-ml train churn_prediction.yaml
Serving
Unified serving layer:
from hanzo.ml import ServingEngine
engine = ServingEngine()
# Register model for serving
engine.register("churn_prediction", model)
# Inference
prediction = engine.predict(
model="churn_prediction",
features={"customer_id": "cust_123"}
)
Automatic batching, caching, and load balancing.
Monitoring
Automatic drift detection:
from hanzo.ml import Monitor
monitor = Monitor("churn_prediction")
# Alerts when:
# - Feature distributions shift
# - Prediction distributions change
# - Performance metrics degrade
# - Latency increases
Results
After migration:
- Training time: 3 hours → 20 minutes (feature reuse)
- Serving latency: 45ms → 8ms (optimized serving)
- Model deployment: 2 days → 2 hours (unified pipeline)
- Bug rate: 60% reduction (shared infrastructure)
Open Source
We are open-sourcing the core framework:
pip install hanzo-ml
Commerce-specific features and managed infrastructure require Hanzo Cloud.
Lessons Learned
- Start with features: Feature store should come first
- Invest in observability: You cannot improve what you cannot measure
- Embrace constraints: Domain-specific framework beats general-purpose
- Test in production: Staging never matches reality
What's Next
- AutoML capabilities
- Distributed training
- Model explanation
- A/B testing integration
Build ML systems that scale. The framework is available now.
Zach Kelling is the founder of Hanzo Industries.
Read more
LLM Infrastructure: Running AI at Scale
How we built infrastructure to serve billions of LLM requests for commerce applications.
Cloud-Native Commerce: Why Architecture Matters
How we built Crowdstart as a cloud-native platform from day one, and why this matters for commerce.
Half a Million RPS on a GB10: The Network Was the Bottleneck, Not the Box
We set out to see how fast our cloud data plane could go on an NVIDIA GB10. The answer taught us more about NIC receive queues than about our own code — and it more than doubled our throughput on the same wire.