Federated serving with PostgreSQL and Redis

Open In Colab

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:

Postgres
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);
Result

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:

Redis
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}'
Result

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:

FeatureQL
DROP FEATURES IF EXISTS IN FM.TUTORIALS.ONLINE_FEDERATED UP TO LEVEL 9;
Result
feature_name VARCHARstatus VARCHARmessage VARCHAR

Plan the federated lookup

For one bound customer ID, the model must:

  1. query the matching PostgreSQL customer row;
  2. extract its typed last_order_id;
  3. build the Redis key for that order;
  4. read and parse the JSON payload; and
  5. 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:

FeatureQL
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]
;
Result
feature_name VARCHARstatus VARCHARmessage VARCHAR
FM.TUTORIALS.ONLINE_FEDERATED.CUSTOMERSCREATEDFeature created as not exists
FM.TUTORIALS.ONLINE_FEDERATED.ORDERSCREATEDFeature created as not exists
FM.TUTORIALS.ONLINE_FEDERATED.CUSTOMER_IDCREATEDFeature created as not exists
FM.TUTORIALS.ONLINE_FEDERATED.POSTGRES_SOURCECREATEDFeature created as not exists
FM.TUTORIALS.ONLINE_FEDERATED.REDIS_SOURCECREATEDFeature created as not exists
FM.TUTORIALS.ONLINE_FEDERATED.CUSTOMER_DETAILSCREATEDFeature created as not exists
FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_IDCREATEDFeature created as not exists
FM.TUTORIALS.ONLINE_FEDERATED.REDIS_KEYCREATEDFeature created as not exists
FM.TUTORIALS.ONLINE_FEDERATED.ORDER_DETAILS_JSONCREATEDFeature created as not exists
FM.TUTORIALS.ONLINE_FEDERATED.ORDER_DETAILSCREATEDFeature created as not exists
FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_CENTSCREATEDFeature created as not exists

The registry now knows both source definitions. Install both connections into the running serving process:

FeatureQL
REFRESH FEATURES
    FM.TUTORIALS.ONLINE_FEDERATED.POSTGRES_SOURCE,
    FM.TUTORIALS.ONLINE_FEDERATED.REDIS_SOURCE
;
Result
FEATURE VARCHARKIND VARCHARSTATUS VARCHARMESSAGE VARCHAR
FM.TUTORIALS.ONLINE_FEDERATED.POSTGRES_SOURCESOURCE_JDBCREFRESHED(empty)
FM.TUTORIALS.ONLINE_FEDERATED.REDIS_SOURCESOURCE_REDISREFRESHED(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:

FeatureQL
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
;
Result
CUSTOMER_ID BIGINTLAST_ORDER_ID BIGINTLAST_ORDER_PRICE_CENTS BIGINT
11011999
22021274
33036000

The output makes the path easy to check:

  • customer 1 → order 101 → 1999 cents;
  • customer 2 → order 202 → 1274 cents;
  • customer 3 → order 303 → 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:

FeatureQL
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
);
Result
feature_name VARCHARstatus VARCHARmessage VARCHAR
FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_PSCREATEDFeature created as not exists

Refresh the compiled definition into the serving process:

FeatureQL
REFRESH FEATURES FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_PS;
Result
FEATURE VARCHARKIND VARCHARSTATUS VARCHARMESSAGE VARCHAR
FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_PSPREPARED_STATEMENTREFRESHED(empty)

Send two separate requests. The first asks for customers 2 and 1; the second asks only for customer 3:

Prepared
FM.TUTORIALS.ONLINE_FEDERATED.LAST_ORDER_PRICE_PS
Call 1
{"input_table_1": [[2], [1]]}
CUSTOMER_ID BIGINTLAST_ORDER_PRICE_PS BIGINT
11999
21274
Call 2
{"input_table_1": [[3]]}
CUSTOMER_ID BIGINTLAST_ORDER_PRICE_PS BIGINT
36000

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.