Preserve forecasting continuity when product codes change. Build a REST API that links old and new codes into product families.
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.
Only the current code's history is visible. Years of data are lost.
All predecessors are linked. Full history is preserved.
A shampoo product goes through two code changes over three years. Here's how the chain system tracks it:
Product code 1001 is introduced in Poland (PL) as an IPC code.
The formula is updated. A new code 1002 is introduced, chained to 1001, and 1001 is discontinued. The system records the link.
Repackaged into a new size. Code 1003 replaces 1002. Same process: introduce, chain, discontinue.
The resulting Product Family:
Product Family PL-IPC-0001 — all three codes share the same demand history.
Families can also split (one code becomes two) or merge (two codes become one), forming a directed acyclic graph (DAG):
A single code's lifespan — from introduction to discontinuation. One code can have multiple non-overlapping generations.
A link between two codes: a successor replaces a predecessor. Creates edges in the product family graph.
All generations connected through chains form a family (DAG). Forecasting aggregates demand across the entire family.
Look up which family a code belongs to on a given date, or find all active codes in a family on a given date.
Each event contains one or more transitions. There are three types:
| Type | What It Does | Required 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 |
{
"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
}
]
}
Your API must reject these invalid scenarios with HTTP 400:
| # | Invalid Scenario | Reason |
|---|---|---|
| 1 | Double introduction of an active code | Overlapping generations |
| 2 | Overlapping generation (intro at earlier date) | Time intervals must not overlap |
| 3 | Double discontinuation | Code already discontinued |
| 4 | Chain with non-existing discontinuation code | Predecessor must exist |
| 5 | Chain with non-existing introduction code | Successor must exist |
| 6 | Discontinuation of never-introduced code | Nothing to discontinue |
| 7 | Chain missing discontinuation_code | Structurally invalid |
| 8 | Intro missing introduction_code | Structurally invalid |
| 9 | Discont missing discontinuation_code | Structurally invalid |
| 10 | Chain where intro == discont code | Cannot self-replace |
| 11 | Invalid country code | Reference data missing |
| 12 | Invalid code type | Reference data missing |
| 13 | Missing date | Structurally invalid |
The provided test script validates your API in three phases:
POST /api/setup/ seeds reference data (countries & code types) and returns HTTP 200.
~100 events are posted. Valid events must return 201, invalid ones must return 400. Then recomputation is triggered.
Resolve and reverse-resolve queries verify that families were computed correctly.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/setup/ | Seed reference data, return 200 |
| Method | Endpoint | Description |
|---|---|---|
| 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 |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/product-families/ | List all families |
| GET | /api/product-families/{id}/ | Get family with generations |
| POST | /api/product-families/recompute/ | Trigger recomputation |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/resolve/ | Code + date + country → family |
| GET | /api/resolve/reverse/ | Family + date → active codes |
| POST | /api/resolve/bulk/ | Bulk resolve multiple codes |
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"
| PL | Poland |
| DE | Germany |
| US | United States |
| JP | Japan |
| CN | China |
| IPC | Internal Product Code |
| GTIN | Global Trade Item Number |
After all events are ingested and the recompute endpoint is called, your system must:
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.
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.
http://localhost:5050.python test_api.py --base-url http://localhost:5050 --total 100
Good luck!