TT Lab
Get started
Learn Learning paths Courses

Integration and Deployment

A Field Was Renamed and Only the Total Went Quietly Wrong

Continue in TT Lab

Goal

You put 1.3 and 1.4 of the partner order API side by side and decide at the field level what breaks us, then build a consumer contract listing only what we read and a contract test that checks that contract every time. You also attach a reading layer that accepts the old and new versions at the same time.

Why it matters

An incident where the connection drops sets off an alert. An incident where a field changes its name does not. rec.get("region") returns None instead of raising an exception, an added enumerated value enters none of our branches, and when an integer becomes a decimal string, the total quietly changes. So the safety device in integration is not "reading the documentation well" but a contract that a machine checks every time. In the contract you write not the partner's whole schema but only the fields we actually read. If you write everything, the light turns red even for changes to fields we do not use, and when the noise grows, people turn the tests off. An upgrade also does not finish all at once. While the old and new versions run together for months, the reading side must accept both, so you gather the places where names changed into an alias table and the places where types changed into a normalization function, in one place. The grader does not believe your sentences. It starts the partner server you built directly on a port the grader chooses, receives the responses, and reruns your decider and comparator with inputs the grader creates to check the answers.

Steps

  1. Create /root/contract/partner.py, run it on port 8011, and save the responses of the two versions to /root/contract/v13.json and /root/contract/v14.json.
  2. Write the field differences between the two responses in /root/contract/diff.json in four slots: added, removed, type_changed, and enum_added.
  3. Create /root/contract/breaking.py so that it takes one change and decides whether it breaks us.
  4. Write a consumer contract listing only the fields we read in /root/contract/order.contract.json.
  5. Create /root/contract/validate.py so that it compares the contract with records and writes violations split into missing, type, and enum.
  6. Create /root/contract/read_order.py so that it moves both 1.3 and 1.4 responses into the same internal shape.
  7. Create /root/contract/contract_test.sh so that it compares the partner's current response with the contract and ends with a non-zero code if they differ.
  8. Report in four sections in /root/contract/contract_report.md.

Notes

Start a partner that serves both versions

Create /root/contract/partner.py, run it on port 8011, and save the responses of /v1.3/orders and /v1.4/orders to /root/contract/v13.json and /root/contract/v14.json respectively. There are 24 orders in both versions.

Make three routes with flask. /health is the place that signals readiness, and the two versions' list routes return the same 24 orders, each with its own field names and types. Take --port and --drift with argparse. If you start it in the foreground the terminal is blocked, so start it in the background and wait until /health is 200.

Extract the difference between the two versions in machine-readable form

In /root/contract/diff.json, write four slots: added, removed, type_changed, and enum_added. added and removed are sorted lists of field names, type_changed is a list of {"field": ..., "from": ..., "to": ...} (the type names are integer and string), and enum_added is a list of {"field": ..., "values": [...]}. Only fields with 6 or fewer distinct values in the new version are treated as enumerations.

Looking at only the first record of the two response files gives you the set of field names, but enumerated values show only if you scan everything. Two type names, integer and string, are enough. For a field with only a few kinds of values such as status, subtract the two sides' value sets.

Decide what breaks us

Create /root/contract/breaking.py so that it decides one change received with --change <파일> (the placeholder is the file) and outputs {"breaking": true|false, "reason": "..."}. The rules for the response side and the request side differ.

The response is the side we read, so only additions are safe. The request is the side we send, so the direction is reversed — an optional field being added or a required field being relaxed to optional is nothing to us. A single table keyed on the (where, kind) pair is enough, and for an unknown pair, answer that it breaks, not that it is safe.

A contract listing only what we read

Write the consumer contract in /root/contract/order.contract.json. version is 1.4, unknown_fields is ignore, and in fields list only the five fields we actually read (order_id, amount, currency, status, market), each with type and required. Add an enum to currency, status, and market too.

You will want to write every field the partner provides, but if you list the fields we do not read (channel, updated_at), our test turns red every time they change those fields. The type names are the three string, integer, and decimal_string, and the amount in 1.4 is a decimal string. Get status's enumerated values by scanning the whole 1.4 response.

Check the contract against the actual response

Create /root/contract/validate.py so that it takes --contract and --records and outputs {"records": n, "ok": n, "violations": [...]}. Split violations into three, missing, type, and enum, and attach index and field to each violation.

A single record can have two violations, so violations can be several lines per record. On the other hand, ok is the number of records with no violation at all, so it does not come from subtracting the number of violation lines. A missing field that is not required is not a violation, and do not check the enumeration again on a value whose type is already wrong.

Accept the old and new versions together

Create /root/contract/read_order.py so that it reads both 1.3 and 1.4 responses with --in <응답 파일> (the placeholder is the response file) and moves them into the same internal shape. Each record has five slots: order_id, amount_krw (integer), currency, status, and market, and unknown fields are dropped.

Gather the places where names changed into one alias table and the places where types changed into one normalization function. If you scatter ifs around, you will not know where to delete when you cut off the old version. Do not go through float for the amount — if you cut "12300.00" at the dot and convert it to an integer, no rounding appears.

Catch a partner that moved again without notice

Create /root/contract/contract_test.sh so that, with bash contract_test.sh <BASE_URL>, it compares the partner's current /v1.4/orders with the contract. It must end with 0 if there are no violations and 1 if there are, and when there are violations, the number must remain on the screen.

Use the validate.py and order.contract.json you built before as they are. All that is new is the part that fetches the response and the exit code. Run it against a partner started with --drift and confirm it gives 1, and against a normal partner and confirm it gives 0.

Version upgrade inspection report

In /root/contract/contract_report.md, write four sections, ## 무엇이 바뀌었나, ## 무엇이 우리를 깨뜨리나, ## 우리가 지킬 계약, and ## 다음부터 어떻게 잡나 (in order: what changed, what breaks us, the contract we keep, how we catch it from now on). The field names and violation counts you obtained in the previous steps must be in the body.

The reader may be not our team lead but someone at the partner company. Write not "it broke" but "which field changed how, and what on our side went wrong." Leave the four section titles as they are and fill in the numbers with the values you obtained.