Задача и границы#
Изменение формата входного события не должно незаметно менять финансовый смысл старых данных. Нужно определить, какие сочетания отправителя и получателя допустимы, как обрабатывать исторические записи и когда блокировать выпуск витрины.
Здесь предлагается инженерный контракт для интеграции событий, а не бухгалтерское нормативное требование или описание опыта автора. Алгоритм сверки ключей рассматривается отдельно в сверке наборов событий. Примеры синтетические; исполняемого SQL или Python нет. Статья не подтверждает поведение какой-либо СУБД или установленной библиотеки.
Зерно и независимые версии#
Входное зерно - одна версия исходной строки финансового события. Ключ логической строки в этом примере: (source_system, legal_entity, event_id, line_id). event_version обозначает исправление её содержания по контракту источника. schema_version определяет формат и согласованное значение полей. Переименование поля не является новой финансовой операцией; новая редакция суммы не становится сменой схемы.
business_date задаёт учётную дату, observed_at - первое получение данной версии события EDWH. Время повторной обработки сохраняют отдельно: оно не заменяет первоначальную известность. Если меняется определение самой известности, это изменение контракта времени, которое надо явно отразить в новых выпусках.
Сумма представлена знаковым целым коэффициентом amount_units, валюта обязательна, масштаб s задаётся контрактом: значение в основных единицах равно amount_units / 10^s. В примере дебет положителен, кредит отрицателен. Это соглашение источника, а не универсальная форма проводки. Масштаб хранения не следует автоматически из кода валюты; подробности - в денежной точности.
| Идентификатор |
Что закрепляет |
schema_version |
Неизменяемое описание формата и семантики входа |
event_version |
Редакцию конкретного финансового события |
adapter_version |
Правила преобразования в каноническую модель EDWH |
release_id |
Входной снимок и зависимости конкретного выпуска |
Если источник не передаёт версию схемы в сообщении, её можно закрепить в манифесте однородной партии. Смешанная партия требует однозначной версии для каждой записи. Определять версию по дате доставки или величине суммы опасно: старые форматы могут прийти поздно.
Совместимость имеет направление#
В этой статье обратная совместимость означает, что новый получатель читает старый вход; прямая - что старый получатель читает новый вход. Двусторонняя требует обоих направлений. Следует проверять все сохраняемые версии, а не только соседние: переходы V1 к V2 и V2 к V3 сами по себе не доказывают чтение V1 получателем V3.
По спецификации Avro 1.12.0 отсутствующее в записи отправителя поле получателя читается с его default; без default возникает ошибка разрешения схем. Default не делает поле необязательным при записи. Лишнее поле отправителя может быть проигнорировано получателем. Эти правила формата не доказывают корректность финансовой интерпретации. Для логического типа decimal при разрешении схем должны совпадать precision и scale.
В JSON Schema Draft 2020-12 required проверяет наличие свойства, но само по себе не запрещает null: нужны отдельные ограничения типа. default является аннотацией, а не предписанием заполнять пропуски. Добавление обязательного свойства делает старый объект без него невалидным по новой схеме. Политика неизвестных полей тоже влияет на совместимость и должна быть закреплена.
Это требования соответствующих спецификаций к формату и валидации. Направления совместимости и правила допуска партии здесь заданы проектным контрактом; бухгалтерский стандарт их не устанавливает. Проверка JSON или успешное чтение Avro не заменяет предметные проверки валюты, знака и назначения суммы.
Три изменения с разными последствиями#
| Изменение |
Риск |
Предлагаемое решение |
Обязательный cost_center в V2 |
В V1 признак отсутствовал |
Отдельный адаптер V1, подтверждённое историческое обогащение или карантин |
| Масштаб RUB с 2 в V1 до 3 в V2 |
То же целое означает другую сумму |
Преобразование по закреплённой версии, проверка диапазона и остатка |
amount из валовой суммы превращён в чистую |
Тип и имя прежние, смысл другой |
Новое семантическое поле или явно несовместимая версия |
Нельзя подставлять произвольный центр затрат только ради прохождения required. Если допустима категория «Неизвестно», она должна быть разрешена владельцем данных и явно исключена из расчётов, где нужен реальный центр. Историческое обогащение закрепляет версию справочника и основание выбора на нужную дату; текущее соответствие не доказывает прошлое.
При переходе от валовой суммы к чистой недостаточно изменить описание поля. Без отдельной суммы налога и согласованного правила расчёта восстановить старый смысл может быть невозможно. Аналогично, замена даты операции датой проводки меняет отбор периода даже при неизменном типе даты.
Синтетическая арифметическая иллюстрация: V1 передаёт 12345 при s = 2, то есть 123.45 RUB; V2 для той же суммы передаёт 123450 при s = 3. При каноническом масштабе 2 первое значение сохраняется, второе делится на 10 без остатка. Но 123451 в V2 означает 123.451 RUB и не представимо точно при масштабе 2. Контракт должен выбрать более точную модель, разрешённое округление с протоколом потери либо отказ. Молчаливое целочисленное деление недопустимо. Для отрицательных значений действуют те же требования сохранения значения и явной политики округления.
Процесс перехода и повторной обработки#
Предпочтительный вариант для ограниченного числа форматов - отдельные адаптеры поддерживаемых версий к одной канонической модели. Это локализует различия, но каноническая модель должна сохранять нужную точность и семантику. Альтернатива - отдельные версионные витрины: меньше принудительной нормализации, больше сложности у потребителей. Одномоментный переход всех участников возможен только при контролируемой координации и не решает чтение архивов.
Следующая последовательность - псевдокод процесса, не проверенная реализация:
Закрепить исходные байты, манифест и версию схемы каждой записи.
Выбрать явно поддерживаемую пару схемы и адаптера.
Проверить структуру по исходному контракту.
Преобразовать единицы, знаки и атрибуты без скрытых потерь.
Сохранить происхождение обогащения и причины отказов.
Проверить канонический контракт и финансовые инварианты.
Зафиксировать новый выпуск с входами и версиями зависимостей.
Переключить потребителей только после сравнения результатов.
До перехода фиксируют матрицу отправителей, получателей и архивных версий, критерии допуска и порядок отката. Сначала получатели учатся принимать новую версию, затем источник начинает её отправлять, если такой порядок подтверждён матрицей. Неизвестная версия блокируется, а не интерпретируется «как последняя».
Старая партия читается по своей исходной схеме. Для точного повторения прежнего выпуска нужны прежние адаптер, справочники и правила; для исправленного расчёта выбирают новые зависимости и создают отдельный выпуск. Сохраняют содержимое схем, а не только URL или идентификатор реестра, а также исходный снимок, версии исполняемых артефактов и параметры. Результат не дописывают как новые события в тот же финансовый набор: повторная обработка не должна удваивать проводки. Правила идентичности и фиксации рассмотрены в инкрементальных загрузках.
Инварианты и граничные случаи#
После преобразования сравнивают сумму в одной согласованной единице отдельно по документу и валюте, а также положительные и отрицательные движения. Нулевой общий итог не доказывает правильность масштаба. Сохраняют ключ и версию события, исходное значение, результат и версию адаптера; для разрешённого округления отдельно отражают разницу. Если зерно выхода меняется, требуется явное правило разложения и контроль его суммы, а не сравнение числа строк один к одному.
Перед допуском нужны сценарии отсутствующего поля, явного null, неизвестного значения перечисления, смешанных версий партии, поздней V1 после запуска V2, отрицательной суммы, переполнения при увеличении масштаба и ненулевого остатка при его уменьшении. Проверяют также повтор старой партии с прежним адаптером и сравнение выпусков после его исправления. Это рекомендуемые проверки будущей реализации, а не утверждение о выполненных тестах примеров.
Карантин сохраняет исходную запись, причину и версию контракта. Частично принятая партия не считается полностью корректной. Исправленный адаптер требует оценки затронутых партий и периодов; новая версия схемы сама по себе не определяет границу пересчёта.
Карантин и состояния исключения#
Карантин в предлагаемом контракте - сохранённое исключение из допуска, а не удаление плохой строки. Зерно журнала исключений - одна причина отказа для исходной версии строки при конкретной попытке проверки. exception_id связывает неизменяемые исходные байты, batch_id, позицию в поставке, ключ и версию события, если их удалось прочитать, версии схемы, адаптера и проверки. Для нечитаемой строки достаточно идентификатора поставки и позиции: выдумывать финансовый ключ нельзя. Несколько причин могут относиться к одной версии; число исключений не равно числу отклонённых строк.
Журнал хранит код причины и диагностические сведения, ответственного за решение, время обнаружения и переходов, основание решения, ссылки на повторную попытку и исправляющую версию. Если валюта или масштаб не определены, сумму помечают как неизвестную, а не как ноль. Контроль ведут отдельно по юридическому лицу, партии, периоду и валюте; неопределённый период блокирует все потенциально затронутые выпуски до уточнения периметра. Исходные данные остаются в защищённом контуре с согласованными правами и сроками хранения; публичная статья не содержит их.
| Состояние |
Условие перехода |
Влияние на допуск |
| Открыто |
Проверка отклонила исходную версию |
Строка не участвует в окончательном результате |
| Ожидает исправления |
Назначен владелец, сохранены причина и требуемое действие |
Исключение остаётся блокирующим |
| Повторная проверка |
Получена исправляющая версия либо утверждён исправленный адаптер |
Это ещё не допуск; новый отказ сохраняет историю |
| Разрешено допуском |
Все причины устранены, новая попытка прошла предметные контроли |
Зафиксированы принятая версия, партия допуска и зависимые периоды |
| Разрешено исключением из периметра |
Владелец утвердил доказанное отсутствие строки в согласованном периметре |
Сохранены основание и новая версия периметра; строка не считается принятой |
Переходы добавляют историю, не переписывают первоначальную причину. Закрывать исключение по таймауту или числу повторов нельзя. Окончательный отказ без доказанного решения оставляет выпуск заблокированным. Если одна строка отклонена по двум причинам, устранение одной не разрешает вторую.
Повтор доставки, исправление и повторный допуск#
Технический повтор приносит те же исходные байты и версию события. Он добавляет сведения о доставке, но не новую экономическую операцию и не доказательство исправления. Изменённое содержание под прежней идентичностью версии считается конфликтом по контракту источника. Исправление источником сохраняют как новую версию с явной связью с отклонённой. Исправление адаптера оставляет вход прежним, но закрепляет новую версию преобразования и причину повторной обработки. Первоначальный observed_at не заменяют временем повтора; известность исправляющей версии фиксируют отдельно.
В документации Apache Kafka 4.1 доставка at-least-once допускает повторную обработку; запись во внешнюю систему требует согласования позиции потребителя с сохранённым результатом. Это свойство конкретной технологии, а не гарантия финансового допуска или требование использовать Kafka. Приведённые состояния карантина - проектное решение, не требование Avro, JSON Schema или бухгалтерского стандарта. Поведение транзакций какой-либо СУБД здесь не проверяется.
Синтетический сценарий: партия B1 содержит версию V1 строки RUB с отсутствующим обязательным центром затрат. Повтор B1 оставляет её отклонённой. Источник присылает V2 в B2 с подтверждённым центром затрат; после проверок V2 связывают с исключением V1 и партией B2. V1 не включают дополнительно, поэтому повторный допуск не удваивает движение. Если источнику нужны отдельные сторнирующие события, применяется его контракт коррекции, а не автоматическая замена. Для разных валют разрешение исключений проверяют раздельно.
Перед допуском проверяют исходный и канонический контракты, все открытые причины, валюту, масштаб, знак, предметные инварианты и отсутствие повторного включения. Сохраняют результат попытки, версию принятого входа и связь с первоначальной партией. При изменении исторического периода определяют зависимые расчёты; уже закреплённый выпуск не редактируют молча.
Исключения и готовность выпуска#
Для каждого выпуска нужен список обязательных поставок и связанных исключений. Перенос строки в карантин не означает завершённость партии. Окончательный выпуск в заявленном периметре запрещён, пока хотя бы одна обязательная строка имеет неразрешённую причину. Разрешение всех исключений необходимо, но само по себе не доказывает полноту поставки. Если бизнес допускает предварительный результат, он явно обозначается неполным, содержит список пропусков и не подменяет окончательный выпуск. Неизвестная сумма не позволяет обещать денежную оценку влияния.
Для доказанного исключения из периметра требуется явное утверждение нового периметра и его версии; нельзя закрыть ошибку лишь потому, что итоговый баланс равен нулю. Контракт поставки определяет завершённость входа, сверка событий проверяет охват, а закрытие периода определяет выпуск результата. Здесь добавлен только контракт разрешения отклонений.
Критерии приёмки будущей реализации: повтор без исправления не закрывает исключение; две причины разрешаются независимо; конфликт версии не принимается; новая попытка не удваивает движение; позднее исправление сохраняет старый выпуск; неизвестная валюта не превращается в нулевую сумму. Это сценарии для проверки, не результаты исполненных тестов. Дополнительный вопрос для подготовки: какое доказательство позволяет связать исправленную строку с исходным отказом и разрешить все его причины?
Вопросы для подготовки#
- Почему default позволяет прочитать старое событие, но не подтверждает его финансовый смысл?
- Что произойдёт с суммой, если масштаб выбрать по дате доставки вместо версии исходного контракта?
- Какие зависимости нужны для повторения старого выпуска и какие меняются при исправленном пересчёте?
Problem and scope#
A change to an incoming event format must not silently change the financial meaning of historical data. The contract needs to define which producer and consumer combinations are allowed, how historical records are processed, and when a mart release must be blocked.
This article proposes an engineering contract for event integration. It is neither an accounting requirement nor a description of the author's experience. The key reconciliation algorithm is covered separately in event-set reconciliation. Examples are synthetic and contain no executable SQL or Python. The article does not verify the behavior of any database or installed library.
Grain and independent versions#
The input grain is one version of a source financial event line. In this example, the logical line key is (source_system, legal_entity, event_id, line_id). event_version identifies a revision to its contents under the source contract. schema_version defines the format and agreed meaning of the fields. Renaming a field does not create a new financial transaction; revising an amount does not constitute a schema change.
business_date determines the accounting date, while observed_at records when the EDWH first received that event version. The time of reprocessing is stored separately and does not replace the original observation time. A change to the definition of when information became known is a change to the time contract and must be reflected explicitly in new releases.
The amount is represented by a signed integer coefficient, amount_units. Currency is mandatory, and scale s is defined by the contract: the value in major units is amount_units / 10^s. Debits are positive and credits negative in this example. This is a source convention, not a universal journal representation. Storage scale must not be inferred automatically from the currency code; see monetary precision.
| Identifier |
What it fixes |
schema_version |
An immutable description of the input format and semantics |
event_version |
The revision of a particular financial event |
adapter_version |
Transformation rules for the canonical EDWH model |
release_id |
The input snapshot and dependencies of a particular release |
If the source does not include a schema version in the message, it can be fixed in the manifest of a homogeneous batch. A mixed batch needs an unambiguous version for every record. Inferring the version from arrival time or amount magnitude is unsafe: older formats can arrive late.
Compatibility has a direction#
Here, backward compatibility means that a new consumer reads old input; forward compatibility means that an old consumer reads new input. Compatibility in both directions requires both conditions. Every retained version should be checked, not just adjacent versions: V1 to V2 and V2 to V3 transitions alone do not prove that a V3 consumer can read V1.
Under the Avro 1.12.0 specification, a reader field missing from the writer's record is read using its default; without a default, schema resolution fails. A default does not make a field optional when writing. A writer's extra field may be ignored by the reader. These format rules do not establish correct financial interpretation. For the decimal logical type, precision and scale must match during schema resolution.
In JSON Schema Draft 2020-12, required checks that a property is present but does not itself prohibit null; separate type constraints are needed. default is an annotation, not an instruction to fill missing values. Adding a required property makes an old object without that property invalid under the new schema. The policy for unknown fields also affects compatibility and must be fixed explicitly.
These are requirements of the respective specifications for format and validation. Compatibility directions and batch acceptance rules are defined here by the project contract, not by an accounting standard. Valid JSON or successful Avro reading does not replace domain checks of currency, signs, and the purpose of an amount.
Three changes with different consequences#
| Change |
Risk |
Proposed response |
Mandatory cost_center in V2 |
V1 did not contain the attribute |
A separate V1 adapter, substantiated historical enrichment, or quarantine |
| RUB scale changes from 2 in V1 to 3 in V2 |
The same integer represents a different amount |
Conversion using the fixed version, with range and remainder checks |
amount changes from gross to net |
The type and name stay the same, but the meaning changes |
A new semantic field or an explicitly incompatible version |
An arbitrary cost center must not be inserted merely to satisfy required. If an "Unknown" category is permitted, the data owner must authorize it, and calculations requiring a real cost center must explicitly exclude it. Historical enrichment fixes the reference-data version and the basis for selecting a value on the relevant date; a current mapping does not establish a historical one.
Changing a field description is insufficient when switching from gross to net. Without a separate tax amount and an agreed calculation rule, restoring the old meaning may be impossible. Similarly, replacing the transaction date with the posting date changes period selection even if the date type stays the same.
Synthetic arithmetic illustration: V1 sends 12345 at s = 2, meaning 123.45 RUB; V2 sends 123450 at s = 3 for the same amount. At canonical scale 2, the first value is retained and the second is divided by 10 without a remainder. However, 123451 in V2 means 123.451 RUB and cannot be represented exactly at scale 2. The contract must choose a more precise model, authorized rounding with a loss record, or rejection. Silent integer division is unacceptable. Negative values have the same requirements for preserving value and applying an explicit rounding policy.
Migration and reprocessing#
For a limited number of formats, a preferred option is a separate adapter for each supported version into one canonical model. This contains the differences, but the canonical model must preserve the required precision and semantics. An alternative is to maintain separate versioned marts: less forced normalization, but more complexity for consumers. A simultaneous migration of all participants is feasible only with controlled coordination and does not solve archive reading.
The following sequence is process pseudocode, not a tested implementation:
Fix the original bytes, manifest, and schema version of every record.
Select an explicitly supported schema and adapter pair.
Validate the structure against the original contract.
Convert units, signs, and attributes without hidden losses.
Record enrichment provenance and rejection reasons.
Check the canonical contract and financial invariants.
Commit a new release with its inputs and dependency versions.
Switch consumers only after comparing results.
Before migration, fix the matrix of producers, consumers, and archive versions, acceptance criteria, and rollback procedure. Consumers first learn to accept the new version, then the source starts sending it, provided the matrix confirms that this order works. An unknown version is blocked rather than interpreted as "the latest."
An old batch is read using its original schema. Exact reproduction of a previous release requires its previous adapter, reference data, and rules; a corrected calculation selects new dependencies and creates a separate release. Retain schema contents rather than only a URL or registry identifier, along with the original snapshot, executable artifact versions, and parameters. Do not append the result as new events to the same financial dataset: reprocessing must not double journal entries. Identity and commit rules are discussed in incremental loading.
Invariants and edge cases#
After conversion, compare amounts in one agreed unit separately by document and currency, and compare positive and negative movements as well. A zero overall total does not establish that the scale is correct. Retain the event key and version, original value, result, and adapter version; record the difference separately for authorized rounding. If the output grain changes, an explicit decomposition rule and a check of its total are needed instead of a one-to-one row count comparison.
Before acceptance, scenarios should cover a missing field, explicit null, an unknown enumeration value, mixed versions within a batch, a late V1 after V2 starts, negative amounts, overflow when increasing scale, and a nonzero remainder when reducing it. Also check replaying an old batch with the old adapter and comparing releases after an adapter correction. These are recommended checks for a future implementation, not claims that the examples have been tested.
Quarantine retains the original record, the reason, and the contract version. A partially accepted batch is not considered fully correct. A corrected adapter requires an assessment of affected batches and periods; a new schema version alone does not define the recalculation boundary.
Quarantine and exception states#
In the proposed contract, quarantine preserves an admission exception rather than deleting a bad row. The exception log grain is one rejection reason for a source row version in a specific validation attempt. An exception_id links immutable source bytes, batch_id, delivery position, event key and version where readable, and schema, adapter, and check versions. An unreadable row can be identified by delivery and position; a financial key must not be invented. Several reasons can concern one version, so exception counts differ from rejected-row counts.
The log retains a reason code and diagnostic details, the decision owner, detection and transition times, decision evidence, and links to retry attempts and the correcting version. If currency or scale is undetermined, the amount is unknown rather than zero. Controls are scoped separately by legal entity, batch, period, and currency; an undetermined period blocks all potentially affected releases until scope is established. Source data stays within a protected environment under agreed access and retention rules; this public article contains none of it.
| State |
Transition condition |
Admission consequence |
| Open |
Validation rejected the source version |
The row is excluded from the final result |
| Awaiting correction |
An owner is assigned and the reason and required action are retained |
The exception remains blocking |
| Revalidation |
A correcting version arrives or a corrected adapter is approved |
Admission is still pending; another failure preserves history |
| Resolved by admission |
All reasons are resolved and a new attempt passes business controls |
Accepted version, admission batch, and dependent periods are recorded |
| Resolved by scope exclusion |
The owner approves evidence that the row is outside the agreed scope |
Evidence and the new scope version are retained; the row is not counted as accepted |
Transitions append history rather than replacing the original reason. Neither a timeout nor a retry count can close an exception. A final rejection without an evidenced resolution keeps the release blocked. Resolving one of two reasons for a row does not resolve the other.
Redelivery, correction, and readmission#
A technical redelivery carries the same source bytes and event version. It adds delivery evidence, not a new economic operation or proof of correction. Changed content under the same version identity is a conflict under the source contract. A source correction is retained as a new version explicitly linked to the rejected one. An adapter correction keeps the input unchanged but pins a new transformation version and a reprocessing reason. The initial observed_at is not replaced by retry time; the correcting version has its own knowledge time.
The Apache Kafka 4.1 documentation describes at-least-once delivery as allowing repeated processing; writes to an external system require coordination between consumer position and persisted output. This is technology-specific behavior, not a financial admission guarantee or a requirement to use Kafka. The quarantine states are a project decision, not an Avro, JSON Schema, or accounting-standard requirement. No database transaction behavior is tested here.
Synthetic scenario: batch B1 contains RUB row version V1 without a mandatory cost center. Redelivering B1 leaves it rejected. The source sends V2 in B2 with a confirmed cost center; after validation, V2 is linked to the V1 exception and batch B2. V1 is not also included, so readmission does not double the movement. Where the source requires separate reversal events, its correction contract applies instead of automatic replacement. Exception resolution is checked separately for different currencies.
Before admission, validate source and canonical contracts, every open reason, currency, scale, sign, business invariants, and the absence of duplicate inclusion. Retain the attempt outcome, admitted input version, and original-batch link. A change affecting a historical period requires identification of dependent calculations; a pinned release is not silently edited.
Exceptions and release readiness#
Each release needs a list of required deliveries and associated exceptions. Moving a row to quarantine does not complete a batch. A final release for the declared scope is prohibited while any mandatory row has an unresolved reason. Resolving every exception is necessary but does not by itself prove delivery completeness. If the business permits a preliminary result, it is explicitly marked incomplete, lists omissions, and does not replace the final release. An unknown amount prevents a promise of monetary impact quantification.
An evidenced scope exclusion requires explicit approval of a new scope and its version; a zero overall balance cannot justify closing an error. The delivery contract establishes input completeness, event reconciliation checks coverage, and period close governs result release. This addition covers only rejected-row resolution.
Acceptance scenarios for a future implementation: redelivery without correction does not close an exception; two reasons are resolved independently; a version conflict is rejected; a new attempt does not double a movement; a late correction preserves the old release; unknown currency does not become a zero amount. These are proposed checks, not executed test results. An additional preparation question: what evidence links a corrected row to the original rejection and resolves every reason?
Review questions#
- Why can a default allow an old event to be read without establishing its financial meaning?
- What happens to an amount if scale is selected by arrival date instead of the original contract version?
- Which dependencies are needed to reproduce an old release, and which change in a corrected recalculation?