Case Study

Demand Forecasting

Preserve forecasting continuity when product codes change. Build a REST API that links old and new codes into product families.

GitHub Repository See an Example Reference Server

The Problem

Accurate demand forecasting drives production planning, inventory management, and distribution. It depends on historical demand data — knowing how much a product sold in previous months to predict the next.

But product codes change constantly: reformulations, repackaging, regulatory requirements, market restructuring. Each change breaks the demand history — the new code starts with zero history, even though it's the same product.

× Without Chains

Only the current code's history is visible. Years of data are lost.

1003

✓ With Chains

All predecessors are linked. Full history is preserved.

1001 → 1002 → 1003

Walkthrough Example

A shampoo product goes through two code changes over three years. Here's how the chain system tracks it:

1

January 2021 — Introduction

Product code 1001 is introduced in Poland (PL) as an IPC code.

2

July 2022 — First Code Change

The formula is updated. A new code 1002 is introduced, chained to 1001, and 1001 is discontinued. The system records the link.

3

April 2024 — Second Code Change

Repackaged into a new size. Code 1003 replaces 1002. Same process: introduce, chain, discontinue.

The resulting Product Family:

1001 2021-01-01 – 2022-06-30 discontinued
→
1002 2022-07-01 – 2024-03-31 discontinued
→
1003 2024-04-01 – present active

Product Family PL-IPC-0001 — all three codes share the same demand history.

Splits & Merges

Families can also split (one code becomes two) or merge (two codes become one), forming a directed acyclic graph (DAG):

Split

A ↓
B C

Merge

D E
↓ F

Key Concepts

📄

Generation

A single code's lifespan — from introduction to discontinuation. One code can have multiple non-overlapping generations.

🔗

Chain

A link between two codes: a successor replaces a predecessor. Creates edges in the product family graph.

🌐

Product Family

All generations connected through chains form a family (DAG). Forecasting aggregates demand across the entire family.

🔎

Resolution

Look up which family a code belongs to on a given date, or find all active codes in a family on a given date.

Event Transition Types

Each event contains one or more transitions. There are three types:

TypeWhat It DoesRequired Fields
INTRO Introduces a new product code — makes it active introduction_code, date
DISCONT Discontinues an active code — sets its end date discontinuation_code, date
chain Links a successor to a predecessor — creates a family edge introduction_code, discontinuation_code, date

Example Event Payload

{
  "iso_country_code": "PL",
  "transitions_write": [
    {
      "code_type_id": "IPC",
      "type": "INTRO",
      "date": "2023-06-01",
      "introduction_code": 1002
    },
    {
      "code_type_id": "IPC",
      "type": "chain",
      "date": "2023-06-01",
      "introduction_code": 1002,
      "discontinuation_code": 1001
    },
    {
      "code_type_id": "IPC",
      "type": "DISCONT",
      "date": "2023-06-01",
      "discontinuation_code": 1001
    }
  ]
}

Validation Rules

Your API must reject these invalid scenarios with HTTP 400:

#Invalid ScenarioReason
1Double introduction of an active codeOverlapping generations
2Overlapping generation (intro at earlier date)Time intervals must not overlap
3Double discontinuationCode already discontinued
4Chain with non-existing discontinuation codePredecessor must exist
5Chain with non-existing introduction codeSuccessor must exist
6Discontinuation of never-introduced codeNothing to discontinue
7Chain missing discontinuation_codeStructurally invalid
8Intro missing introduction_codeStructurally invalid
9Discont missing discontinuation_codeStructurally invalid
10Chain where intro == discont codeCannot self-replace
11Invalid country codeReference data missing
12Invalid code typeReference data missing
13Missing dateStructurally invalid

How Testing Works

The provided test script validates your API in three phases:

Setup

POST /api/setup/ seeds reference data (countries & code types) and returns HTTP 200.

Event Ingestion

~100 events are posted. Valid events must return 201, invalid ones must return 400. Then recomputation is triggered.

Family Queries

Resolve and reverse-resolve queries verify that families were computed correctly.

API Endpoints

Setup

MethodEndpointDescription
POST/api/setup/Seed reference data, return 200

Events

MethodEndpointDescription
GET/api/events/List all events
POST/api/events/Create event with transitions
GET/api/events/{id}/Get single event
DELETE/api/events/{id}/Delete event

Product Families

MethodEndpointDescription
GET/api/product-families/List all families
GET/api/product-families/{id}/Get family with generations
POST/api/product-families/recompute/Trigger recomputation

Resolution

MethodEndpointDescription
GET/api/resolve/Code + date + country → family
GET/api/resolve/reverse/Family + date → active codes
POST/api/resolve/bulk/Bulk resolve multiple codes

Data Model

The system is built around events containing code transitions, which produce generations grouped into product families.

erDiagram
    COUNTRY {
        string code PK
        string name
    }

    EVENT {
        int id PK
        string iso_country_code FK
        string comment
    }

    CODE_TYPE {
        string id PK
        string type
    }

    CODE_TRANSITION {
        int id PK
        int event_id FK
        string code_type_id FK
        string type
        date date
    }

    INTRODUCTION {
        int id PK
        int code_transition_id FK
        bigint introduction_code
    }

    DISCONTINUATION {
        int id PK
        int code_transition_id FK
        bigint discontinuation_code
    }

    CHAIN {
        int id PK
        int code_transition_id FK
        bigint introduction_code
        bigint discontinuation_code
    }

    PRODUCT_FAMILY {
        int id PK
        string code_type_id FK
        string identifier
        string iso_country_code FK
    }

    GENERATION {
        int id PK
        int product_family_id FK
        int introduction_id FK
        int discontinuation_id FK
    }

    GENERATION_LINK {
        int id PK
        int predecessor_id FK
        int successor_id FK
        int source_transition_id FK
    }

    COUNTRY ||--o{ EVENT : "has"
    COUNTRY ||--o{ PRODUCT_FAMILY : "has"
    EVENT ||--o{ CODE_TRANSITION : "has"
    CODE_TYPE ||--o{ CODE_TRANSITION : "has"
    CODE_TRANSITION ||--o| INTRODUCTION : "is"
    CODE_TRANSITION ||--o| DISCONTINUATION : "is"
    CODE_TRANSITION ||--o| CHAIN : "is"
    CODE_TYPE ||--o{ PRODUCT_FAMILY : "has"
    PRODUCT_FAMILY ||--o{ GENERATION : "has"
    CODE_TRANSITION ||--o{ GENERATION : "introduction"
    CODE_TRANSITION ||--o{ GENERATION : "discontinuation"
    GENERATION ||--o{ GENERATION_LINK : "predecessor"
    GENERATION ||--o{ GENERATION_LINK : "successor"
    CODE_TRANSITION ||--o{ GENERATION_LINK : "source"
        

Reference Data

Countries

PLPoland
DEGermany
USUnited States
JPJapan
CNChina

Code Types

IPCInternal Product Code
GTINGlobal Trade Item Number

Family Computation Logic

After all events are ingested and the recompute endpoint is called, your system must:

  1. Collect all code transitions, grouped by country and code type.
  2. Sort transitions chronologically (by date), with tie-breaking: Introductions first, then Chains, then Discontinuations.
  3. Build a directed graph where:
    • Each Introduction creates a new node (generation).
    • Each Chain creates an edge between two nodes (linking predecessor to successor).
    • Each Discontinuation sets the end date on an existing node.
  4. Identify weakly connected components in the graph — each component is a product family.
  5. Persist the families, generations, and links.

Reference Server (for inspiration only)

A working example of the Chains system is hosted at https://chains.durczok.ovh. It implements the same API surface you are being asked to build and ships with a browsable UI so you can explore the data model, poke at endpoints, and see what “correct” behaviour looks like before (or while) writing your own implementation.

If you would like to play with it, reach out to me directly and I will share a username and password. The server uses HTTP Basic authentication on every request — credentials will be provided out-of-band, so please never commit them to git.

⚠️ Do not use this server as the backend for your assignment

The whole point of the case study is that you build the API. The test script must run against your server on localhost:5050. Pointing it at chains.durczok.ovh would mean all the work is being done by someone else's code — that defeats the purpose of the exercise and will not count as a valid submission.

Treat the reference server purely as a sandbox for learning and inspiration.

If you do want to explore it programmatically (e.g. from a notebook), the authentication pattern with requests looks like this:

import os, requests

BACKEND = requests.Session()
BACKEND.auth = (os.environ["CHAINS_USERNAME"], os.environ["CHAINS_PASSWORD"])

r = BACKEND.get("https://chains.durczok.ovh/api/events/")

See notebooks/server_example.py for a fuller example.

Getting Started

  1. Set up a web server listening on http://localhost:5050.
  2. Implement the event ingestion endpoint with full validation logic.
  3. Implement the family recomputation engine.
  4. Implement the resolution endpoints.
  5. Run the test script against your server and iterate until all tests pass:
python test_api.py --base-url http://localhost:5050 --total 100

Good luck!