← All field notes football result corrections api sync

Apply Football Result Corrections Without Double Counting

A finished status is useful for selecting results, but it does not make every stored field immutable. A score, kickoff or participant correction can arrive after your first export. If your application adds every received score to a season total, a repeated response or corrected result can count the same fixture twice.

Treat a result as revisable state

A finished status is useful for selecting results, but it does not make every stored field immutable. A score, kickoff or participant correction can arrive after your first export. If your application adds every received score to a season total, a repeated response or corrected result can count the same fixture twice.

Use the stable match ID as the identity of the observation and retain the latest validated state for that match. Compute aggregates from those current states or maintain a tested reverse-and-replace ledger. The key decision is replacement by identity, not unconditional addition whenever an HTTP request succeeds.

Choose a small replaceable contribution

The example stores one projected result per match ID and calculates total goals from the current snapshot. Replaying an identical record leaves the total unchanged. Updating the same match from 1-0 to 2-0 changes the total by one, and changing its status to postponed removes its contribution from finished-result totals.

This model is intentionally simple. A production database should wrap the state write and any derived-data invalidation in one transaction. If two workers may process the same match, serialize the update or use a version check so a slower older observation cannot overwrite a newer accepted one.

Run the reproducible example

This Python 3 example uses deliberately synthetic inputs, not a downloaded match or a forecast for a real fixture. Save it as example.py and run python3 example.py. It uses only the standard library and its assertions check the behavior discussed here. Replace the fixture inputs with validated API records when connecting your application; demo identifiers are not production lookup values.

Complete offline example python
state = {}

def accept(record):
    if record["status"] == "finished" and (record["home_team"]["score"] is None or record["away_team"]["score"] is None):
        raise ValueError("incomplete finished result")
    state[record["id"]] = record

def total_goals():
    return sum(r["home_team"]["score"]+r["away_team"]["score"]
               for r in state.values() if r["status"] == "finished")

def result(home, status="finished"):
    return {"id":"match_demo","status":status,"home_team":{"score":home},"away_team":{"score":0}}

accept(result(1))
assert total_goals() == 1
accept(result(1))
assert total_goals() == 1
accept(result(2))
assert total_goals() == 2
accept(result(None,"postponed"))
assert total_goals() == 0 and len(state) == 1
print("replay=1 correction=2 postponed=0; one stored identity")

Read the result

The four stages produce totals of one, one, two and zero while retaining a single match identity. The in-memory fixture proves replacement arithmetic; your database transaction and concurrency behavior still need integration tests.

Do not overinterpret updated_at

The Match schema's updated_at describes when Stats API observed and stored a normalized change. It is not an upstream event timestamp or a guarantee of sporting freshness. Save your retrieved_at value separately. If updated_at is null, do not fabricate a source time from the HTTP response date.

A correction audit should retain the old and new values, match ID, retrieval time and the job that accepted the update. That history helps explain a changed table without suggesting that a newly retrieved response necessarily describes a newly changed sporting event. Keep secrets and complete authorization headers out of the audit.

Test replay, correction and withdrawal paths

Begin with one known result, replay it, correct it and then make it ineligible. Assert the aggregate after every step. Add a duplicate-worker case and a failed transaction test in your actual persistence layer. The aim is to prove the recovery path, not just the happy-path arithmetic.

For large exports, repeat a bounded overlap window so recently corrected fixtures are revisited. A purely forward checkpoint can miss a result whose kickoff belongs to yesterday but whose normalized score changes today. Document your correction window and recovery procedure rather than promising that one historical export will stay permanently accurate.

Connect this to your project

Apply the same identity and replay rules to your local fixture database before using its totals in customer-facing charts. The MySQL export guide provides the persistence context, and the match documentation identifies the fields whose meaning must survive every update.

Read the match contract → Explore the football API → Plan an idempotent database export →