Federated serving with PostgreSQL and Redis
This tutorial follows one customer lookup across PostgreSQL and Redis, then compiles the cross-source graph into a prepared statement.
The caller will provide only a customer ID. FeatureMesh will retrieve that customer's last order ID from PostgreSQL, use it to read order JSON from Redis, and return the order price.
Complete Real-time segmentation first so source refreshes and prepared inputs are familiar. These examples require the serving backend, PostgreSQL, Redis, and both serving executors. Run them from top to bottom.
Split the data across two stores
PostgreSQL owns the customer-to-order relationship. Create three customer rows:
DROP TABLE IF EXISTS tutorial_online_customers;
--
CREATE TABLE tutorial_online_customers (
customer_id BIGINT PRIMARY KEY,
last_order_id BIGINT NOT NULL
);
--
INSERT INTO tutorial_online_customers (customer_id, last_order_id)
VALUES (1, 101), (2, 202), (3, 303);Customer 1 points to order 101, customer 2 to 202, and customer 3 to 303.
Redis owns the order payload. Store one JSON object under each order key:
DEL tutorial:federated:order:101 tutorial:federated:order:202 tutorial:federated:order:303
SET tutorial:federated:order:101 '{"order_id":101,"price_cents":1999}'
SET tutorial:federated:order:202 '{"order_id":202,"price_cents":1274}'
SET tutorial:federated:order:303 '{"order_id":303,"price_cents":6000}'The prices are 1999, 1274, and 6000 cents. Neither store contains the final customer-to-price result by itself.
Reset the tutorial namespace before defining the feature graph:
DROP FEATURES IF EXISTS IN FM.TUTORIALS.ONLINE_FEDERATED UP TO LEVEL 9;| feature_name VARCHAR | status VARCHAR | message VARCHAR |
|---|
Plan the federated lookup
For one bound customer ID, the model must:
- query the matching PostgreSQL customer row;
- extract its typed
last_order_id; - build the Redis key for that order;
- read and parse the JSON payload; and
- return
price_cents.
Map both sources
The model declares CUSTOMERS and ORDERS as distinct entities and creates one JDBC source plus one Redis source.
EXTERNAL_COLUMNS() maps the PostgreSQL table through VIEW(), and BIND TO CUSTOMER_ID retrieves the row whose customer_id matches the bound input. Each column is typed, and last_order_id carries the #ORDERS annotation. The remaining features build the Redis key, read the JSON, parse it into a typed row, and extract the price:
CREATE OR REPLACE FEATURES IN FM.TUTORIALS.ONLINE_FEDERATED AS
PERSIST
CUSTOMERS := ENTITY(),
ORDERS := ENTITY(),
CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
POSTGRES_SOURCE := SOURCE_JDBC(
'postgresql://featuremesh:featuremesh@host.docker.internal:5433/featuremesh?sslmode=disable'
WITH (
tables=ARRAY['tutorial_online_customers'],
timeout='500ms'
)
),
REDIS_SOURCE := SOURCE_REDIS(
'redis://host.docker.internal:6380'
WITH (timeout='500ms')
),
CUSTOMER_DETAILS := EXTERNAL_COLUMNS(
customer_id BIGINT#CUSTOMERS BIND TO CUSTOMER_ID,
last_order_id BIGINT#ORDERS
FROM VIEW(FM.TUTORIALS.ONLINE_FEDERATED.POSTGRES_SOURCE[tutorial_online_customers])
),
LAST_ORDER_ID := CUSTOMER_DETAILS[last_order_id],
REDIS_KEY := 'tutorial:federated:order:' || CAST(KEY_TO_SCALAR(LAST_ORDER_ID) AS VARCHAR),
ORDER_DETAILS_JSON := EXTERNAL_REDIS(KEY REDIS_KEY FROM REDIS_SOURCE),
ORDER_DETAILS := JSON_PARSE_AS(
ORDER_DETAILS_JSON,
TYPE 'ROW(order_id BIGINT, price_cents BIGINT)'
),
LAST_ORDER_PRICE_CENTS := ORDER_DETAILS[price_cents]
;| feature_name VARCHAR | status VARCHAR | message VARCHAR |
|---|---|---|
| FM.TUTORIALS.ONLINE_FEDERATED.CUSTOMERS | CREATED | Feature created as not exists |
| FM.TUTORIALS.ONLINE_FEDERATED.ORDERS | CREATED | Feature created as not exists |
| FM.TUTORIALS.ONLINE_FEDERATED.CUSTOMER_ID | CREATED | Feature created as not exists |
| FM.TUTORIALS.ONLINE_FEDERATED.POSTGRES_SOURCE | CREATED | Feature created as not exists |
| FM.TUTORIALS.ONLINE_FEDERATED.REDIS_SOURCE | CREATED | Feature created as not exists |
| FM.TUTORIALS.ONLINE_FEDERATED.CUSTOMER_DETAILS | CREATED | Feature created as not exists |
| FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_ID | CREATED | Feature created as not exists |
| FM.TUTORIALS.ONLINE_FEDERATED.REDIS_KEY | CREATED | Feature created as not exists |
| FM.TUTORIALS.ONLINE_FEDERATED.ORDER_DETAILS_JSON | CREATED | Feature created as not exists |
| FM.TUTORIALS.ONLINE_FEDERATED.ORDER_DETAILS | CREATED | Feature created as not exists |
| FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_CENTS | CREATED | Feature created as not exists |
The registry now knows both source definitions. Install both connections into the running serving process:
REFRESH FEATURES
FM.TUTORIALS.ONLINE_FEDERATED.POSTGRES_SOURCE,
FM.TUTORIALS.ONLINE_FEDERATED.REDIS_SOURCE
;| FEATURE VARCHAR | KIND VARCHAR | STATUS VARCHAR | MESSAGE VARCHAR |
|---|---|---|---|
| FM.TUTORIALS.ONLINE_FEDERATED.POSTGRES_SOURCE | SOURCE_JDBC | REFRESHED | (empty) |
| FM.TUTORIALS.ONLINE_FEDERATED.REDIS_SOURCE | SOURCE_REDIS | REFRESHED | (empty) |
Refreshing them together ensures the whole dependency graph is available before the first request.
Evaluate the cross-store feature
Bind all three customer IDs and expose the intermediate order ID beside the final price:
RETURN
FM.TUTORIALS.ONLINE_FEDERATED.CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2, 3]),
FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_ID,
FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_CENTS
;| CUSTOMER_ID BIGINT | LAST_ORDER_ID BIGINT | LAST_ORDER_PRICE_CENTS BIGINT |
|---|---|---|
| 1 | 101 | 1999 |
| 2 | 202 | 1274 |
| 3 | 303 | 6000 |
The output makes the path easy to check:
- customer
1→ order101→ 1999 cents; - customer
2→ order202→ 1274 cents; - customer
3→ order303→ 6000 cents.
FeatureMesh follows the dependency graph in order—PostgreSQL first, then Redis. The caller supplies only customer IDs and does not coordinate the two stores.
Prepare the federated feature
Compile LAST_ORDER_PRICE_CENTS and expose CUSTOMER_ID as the prepared input:
CREATE OR REPLACE FEATURE FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_PS AS
PREPARED_STATEMENT(
FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_CENTS
USING FM.TUTORIALS.ONLINE_FEDERATED.CUSTOMER_ID
);| feature_name VARCHAR | status VARCHAR | message VARCHAR |
|---|---|---|
| FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_PS | CREATED | Feature created as not exists |
Refresh the compiled definition into the serving process:
REFRESH FEATURES FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_PS;| FEATURE VARCHAR | KIND VARCHAR | STATUS VARCHAR | MESSAGE VARCHAR |
|---|---|---|---|
| FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_PS | PREPARED_STATEMENT | REFRESHED | (empty) |
Send two separate requests. The first asks for customers 2 and 1; the second asks only for customer 3:
FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_PS{"input_table_1": [[2], [1]]} | CUSTOMER_ID BIGINT | LAST_ORDER_PRICE_PS BIGINT |
|---|---|
| 1 | 1999 |
| 2 | 1274 |
{"input_table_1": [[3]]} | CUSTOMER_ID BIGINT | LAST_ORDER_PRICE_PS BIGINT |
|---|---|
| 3 | 6000 |
The responses remain separate even though the first request contains several input rows. The displayed result sorts rows within the first response: customer 1 at 1999, then customer 2 at 1274. The second returns customer 3 at 6000.
What's next
You have built and prepared a feature whose dependencies cross a relational lookup and a key-value lookup while keeping the request contract to one customer ID.
See Multi-source queries for the general federation pattern. The JDBC source , Redis source , and prepared statement pages cover the individual building blocks in depth.