Перейти до вмісту

Стилі інтеграції

Як системи обмінюються даними: синхронно чи через повідомлення й файли, яким протоколом, як пережити збої сусіда й не створити дублів і як підключити ERP, облікові системи та EDI-провайдерів. На співбесіді тут перевіряють не знання API, а вміння пояснити, чим платиш за кожен вибір.

Перед цією темою: Web API.

Junior+

Чим синхронна інтеграція відрізняється від асинхронної і коли яку обрати?

Section titled “Чим синхронна інтеграція відрізняється від асинхронної і коли яку обрати?”
Відповідь

Що це

  • Синхронна інтеграція — той, хто викликає, надсилає запит і чекає відповіді: REST, gRPC, SOAP.
  • Асинхронна інтеграція — відправник кладе повідомлення в чергу або файл у папку обміну і працює далі. Отримувач обробить його пізніше.

Коли що обирати

Ситуація Стиль
Відповідь потрібна зараз: ціна, залишок, авторизація платежу синхронно
Дані не мають загубитися, відповідь не термінова: замовлення, накладні асинхронно
Великі пакети раз на добу: довідники, вивантаження в облік файли або пакетне завантаження

Типова помилка

Інтегруватися через спільну базу: читати й писати безпосередньо в таблиці чужої системи. Перейменування колонки в сусіда ламає ваш код, а його бізнес-правила обходяться.

Як влаштовано

Стиль Чим пов’язані системи Головний мінус
Файловий обмін форматом файлу й розкладом дані застарівають до наступного вивантаження
Спільна база схемою таблиць схему не можна змінити без сусідів
Віддалений виклик (REST, gRPC, SOAP) часом: обидва мають працювати відмова отримувача — відмова того, хто викликає
Повідомлення через брокер форматом повідомлення затримка, дублі, порядок

Коли що обирати

  • Користувач чекає відповіді, операція коротка — синхронний виклик із тайм-аутом.
  • Операція довга, але клієнту потрібен результат — асинхронний запит-відповідь: сервер одразу відповідає 202 Accepted з адресою, за якою клієнт дізнається статус.
  • Подія цікава кільком системам — повідомлення в брокер, а не виклик кожної.

Пастки

  • Тайм-аут — не відмова. Якщо відповідь не прийшла, невідомо, чи виконалася операція. Повтор без захисту створює дубль.
  • Асинхронно — не означає швидше. Повідомлення може лежати в черзі хвилинами, тому за глибиною черги стежать.
  • Недописаний файл. Отримувач забирає файл, поки його ще пишуть. Файл пишуть під тимчасовим ім’ям і перейменовують наприкінці.

Правило

За замовчуванням між системами — повідомлення або API, спільна база — ніколи. Синхронно — лише там, де без відповіді не можна продовжити.

Під капотом

  • Синхронний виклик тримає ресурси того, хто викликає, — потік, з’єднання, транзакцію — увесь час очікування. Повільний сусід вичерпує їх і перетворює чужу затримку на вашу відмову.
  • Брокер зберігає повідомлення, доки отримувач його не підтвердить. Відмова отримувача стає затримкою, а не помилкою у відправника.
  • Спільна база зв’язує системи найсильніше: сусід залежить від схеми, блокувань, індексів і навіть від вашого вікна обслуговування.

Ціна

  • Доступність синхронного ланцюжка — добуток доступностей ланок: 0,999⁵ ≈ 0,995, тобто майже 44 години простою на рік замість 9.
  • Затримки ланцюжка додаються, а хвіст затримки визначає найповільніший учасник.
  • Асинхронність купується узгодженістю в кінцевому підсумку (eventual consistency): якийсь час системи бачать різні дані.

Компроміси

  • Синхронний виклик простіше писати, налагоджувати й пояснювати користувачу: помилку видно одразу.
  • Асинхронний обмін потребує кореляції запитів і відповідей, обробки дублів, моніторингу черг і «застряглих» повідомлень. Брокери й гарантії доставки — тема «Черги й Kafka».
  • Файли досі нормальний вибір для великих пакетів і партнерів без API, але потрібна домовленість про формат, імена й квитанції.

Як перевірити в продакшені

  • Для синхронних залежностей — p99 і частка помилок вихідних викликів, метрика http.client.request.duration.
  • Для асинхронних — глибина черги, вік найстарішого повідомлення й кількість повідомлень у черзі помилок.

Підсумок: синхронна інтеграція переносить чужі збої до вас одразу, асинхронна — перетворює їх на затримку; спільна база перетворює чужу схему на ваш контракт.

Приклад
using System.Threading.Channels;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(Channel.CreateUnbounded<string>());
builder.Services.AddHostedService<ExportWorker>();
var app = builder.Build();
// Синхронно: відповідь потрібна зараз, клієнт чекає
app.MapGet("/stock/{sku}", (string sku) => new { sku, quantity = 42 });
// Асинхронно: прийняти, поставити в чергу й одразу відповісти 202 Accepted
app.MapPost("/exports/{docId}", async (string docId, Channel<string> queue) =>
{
await queue.Writer.WriteAsync(docId);
return Results.Accepted($"/exports/{docId}"); // за цією адресою клієнт дізнається статус
});
app.Run();
// Отримувач розбирає чергу у своєму темпі.
// Канал у пам'яті втрачає повідомлення під час перезапуску — у продакшені тут брокер.
public class ExportWorker(Channel<string> queue, ILogger<ExportWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var docId in queue.Reader.ReadAllAsync(stoppingToken))
logger.LogInformation("Exported {DocId}", docId);
}
}
Middle+

REST, gRPC чи SOAP: чим відрізняються і що коли обрати?

Section titled “REST, gRPC чи SOAP: чим відрізняються і що коли обрати?”
Відповідь

Що це

  • REST — URL позначає ресурс, HTTP-метод — дію, тіло зазвичай JSON. Докладно — у темі Web API.
  • gRPC — клієнт викликає метод віддаленого сервісу як звичайний метод. Контракт описано у файлі .proto, за ним генеруються класи сервера й клієнта.
  • SOAP — запит і відповідь — XML-конверт (envelope), контракт описано у WSDL. Так працюють сервіси WCF і багато корпоративних систем.

Як користуватися

  • REST — контролери або Minimal API, клієнт — HttpClient.
  • gRPC — пакет Grpc.AspNetCore, файл .proto, AddGrpc() і MapGrpcService<T>().
  • SOAP-клієнт — dotnet-svcutil генерує проксі за WSDL.

Типова помилка

Обрати gRPC для API, яке викликає браузер. Браузер не вміє викликати gRPC напряму, знадобиться gRPC-Web або проксі.

Як влаштовано

Стиль Контракт і формат Сильна сторона
REST OpenAPI (необов’язково), JSON зрозумілий усім, працює в браузері
gRPC .proto (обов’язково), Protobuf, HTTP/2 швидкість, генерація клієнта, потоки
SOAP WSDL і XSD, XML сувора схема, стандарти WS-*

У gRPC чотири види викликів: unary (запит-відповідь), потоковий від сервера, потоковий від клієнта й двонаправлений.

Коли що обирати

Ознака gRPC REST
Клієнт — браузер лише через gRPC-Web так
Внутрішні виклики з високим навантаженням так можна, але важче
Потоки в реальному часі так частково: SSE, WebSocket
Публічний API для партнерів рідко так

Пастки

  • Дедлайн. У gRPC-виклику немає дедлайну за замовчуванням: без deadline виклик може чекати нескінченно.
  • Номери полів. У .proto поле визначається номером. Змінити номер або перевикористати видалений — зламати сумісність.
  • WCF у .NET Core. Клієнтські пакети System.ServiceModel.Http, System.ServiceModel.NetTcp підтримують лише підмножину можливостей WCF із .NET Framework, частина WS-* недоступна.

Правило

Зовнішній і браузерний API — REST, внутрішній потік викликів — gRPC, SOAP — лише на стику з тим, хто його вимагає.

Під капотом

  • gRPC-виклик — окремий потік (stream) HTTP/2. Повідомлення йдуть із префіксом довжини, а статус виклику — у trailers (grpc-status), тому проксі має пропускати HTTP/2 повністю.
  • Одне HTTP/2-з’єднання мультиплексує багато викликів. За L4-балансувальником усі виклики клієнта підуть на один сервер — потрібен L7-балансувальник або балансування на клієнті.
  • Protobuf кодує поле номером і типом. Невідомі поля старий клієнт пропускає, тому додавання поля сумісне, а зміна номера — ні.

Ціна

  • Бінарний Protobuf не прочитати в лозі чи Fiddler без схеми.
  • gRPC-Web у браузері підтримує лише unary і потоки від сервера.
  • SOAP — найважчий: XML-конверт, простори імен, іноді WS-Security з підписом кожного повідомлення.

Компроміси

  • gRPC JSON transcoding (.NET 7+) публікує gRPC-сервіс ще й як REST за анотаціями в .proto: один код, два протоколи.
  • CoreWCF — порт серверної частини WCF від спільноти .NET Foundation з підтримкою Microsoft. Він закриває перенесення, але не все: розподілені транзакції й частина WS-* не підтримуються.
  • Клієнти до чужих SOAP-сервісів генерує dotnet-svcutil; актуальна версія пакетів System.ServiceModel.* — 10.x, вийшла разом із .NET 10.

Як перевірити в продакшені

  • dotnet-counters показує лічильники Grpc.AspNetCore.Server і Grpc.Net.Client: кількість викликів, помилок і перевищень дедлайну.
  • У трасуванні виклики gRPC позначені тегами grpc.method і grpc.status_code, а в SOAP розбирають fault-відповіді: помилка приходить як <Fault> у тілі.

Підсумок: gRPC — продуктивність і суворий контракт ціною браузера й читабельності, REST — універсальність, SOAP — сумісність; CoreWCF — міст для перенесення, а не вибір для нового сервісу.

gRPC: контракт і сервіс
syntax = "proto3";
option csharp_namespace = "Stock";
service StockService {
rpc GetBalance (BalanceRequest) returns (BalanceReply); // unary
rpc WatchBalances (WatchRequest) returns (stream BalanceReply); // потік від сервера
}
message BalanceRequest { string sku = 1; int32 store_id = 2; }
message BalanceReply { string sku = 1; int32 quantity = 2; }
message WatchRequest { int32 store_id = 1; }
Grpc.AspNetCore
using Grpc.Core;
using Stock; // класи згенеровано з .proto під час збирання
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();
var app = builder.Build();
app.MapGrpcService<StockGrpcService>();
app.Run();
public class StockGrpcService : StockService.StockServiceBase
{
public override Task<BalanceReply> GetBalance(BalanceRequest request, ServerCallContext context) =>
Task.FromResult(new BalanceReply { Sku = request.Sku, Quantity = 42 });
}

Клієнт — фрагмент: завжди з дедлайном.

Grpc.Net.Client
using Grpc.Core;
using Grpc.Net.Client;
using Stock;
using var channel = GrpcChannel.ForAddress("https://stock.example.com");
var client = new StockService.StockServiceClient(channel);
try
{
var reply = await client.GetBalanceAsync(
new BalanceRequest { Sku = "SKU-1", StoreId = 17 },
deadline: DateTime.UtcNow.AddSeconds(2)); // без дедлайну виклик чекає нескінченно
Console.WriteLine(reply.Quantity); // 42
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
Console.WriteLine("Stock service timeout");
}
SOAP-клієнт без згенерованого проксі
System.ServiceModel.Http
using System.ServiceModel;
var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
var address = new EndpointAddress("https://erp.example.com/InvoiceService.svc");
var factory = new ChannelFactory<IInvoiceService>(binding, address);
IInvoiceService client = factory.CreateChannel();
string status = await client.GetStatusAsync("INV-1001"); // XML-конверт іде по HTTPS
Console.WriteLine(status);
((IClientChannel)client).Close();
factory.Close();
// Контракт повторює WSDL; зазвичай його генерує dotnet-svcutil
[ServiceContract]
public interface IInvoiceService
{
[OperationContract]
Task<string> GetStatusAsync(string invoiceNumber);
}
Middle+

Як зробити виклик зовнішнього сервісу стійким?

Section titled “Як зробити виклик зовнішнього сервісу стійким?”
Відповідь

Що це

Тимчасовий збій — помилка, яка минає сама: обрив мережі, перезапуск сервісу, відповідь 503. Стійкий виклик переживає такі збої й не зависає, якщо сусід упав надовго.

Як користуватися

  1. Реєструйте клієнта через IHttpClientFactory: builder.Services.AddHttpClient<ErpClient>(...).
  2. Додайте пакет Microsoft.Extensions.Http.Resilience і викличте .AddStandardResilienceHandler().
  3. Передавайте CancellationToken в усі виклики, щоб скасування запиту зупиняло й вихідний виклик.

Типова помилка

Повторювати запит у циклі без паузи й без обмеження. Якщо сусід перевантажений, такі повтори добивають його остаточно.

Як влаштовано

AddStandardResilienceHandler вибудовує п’ять стратегій, від зовнішньої до внутрішньої:

Стратегія Що робить За замовчуванням
Rate limiter обмежує одночасні запити 1000, без черги
Total timeout ліміт на виклик з усіма спробами 30 с
Retry повтор за 5xx, 408, 429 і мережевих помилок 3 рази, експонента від 2 с, jitter
Circuit breaker розмикається за частки помилок 10% за 100+ запитів за 30 с, пауза 5 с
Attempt timeout ліміт на одну спробу 10 с

Circuit breaker — автомат із трьома станами: замкнений (виклики йдуть), розімкнений (виклики одразу відхиляються), напіввідкритий (пробний виклик вирішує, чи замкнутися).

Коли що обирати

  • Будь-який HTTP-клієнт — стандартний обробник, параметри підлаштовують під SLA сусіда.
  • Потрібна запасна відповідь: кеш, значення за замовчуванням — власний конвеєр через AddResilienceHandler зі стратегією fallback.
  • Виклик не через HttpClient, наприклад WCF або SFTP, — Polly v8 напряму: ResiliencePipelineBuilder.

Пастки

  • Повтор POST. Стандартний обробник повторює всі методи. Для неідемпотентних — options.Retry.DisableForUnsafeHttpMethods() або ключ ідемпотентності.
  • Кілька обробників. Два resilience-обробники на одному клієнті повторюють один одного. Документація радить один.
  • HttpClient.Timeout. За замовчуванням він 100 с і діє поверх конвеєра; його роблять не меншим за загальний тайм-аут.

Правило

Тайм-аут — завжди, повтор — лише ідемпотентного й із jitter, circuit breaker — на кожну зовнішню залежність.

Під капотом

  • Обробник — DelegatingHandler у ланцюжку HttpClient, усередині — конвеєр Polly v8. Стратегія, додана першою, — зовнішня.
  • Тайм-аут у Polly v8 кооперативний: скасовує CancellationToken, і код має його поважати. Під час спрацювання — TimeoutRejectedException.
  • Circuit breaker у Polly v8 рахує частку помилок за вікно (FailureRatio, MinimumThroughput, SamplingDuration). Класичного лічильника помилок поспіль у v8 немає. Bulkhead із Polly v7 замінено обмежувачем конкурентності (AddConcurrencyLimiter, пакет Polly.RateLimiting).

Ціна

  • Кожен повтор — додаткове навантаження на сусіда, який і так не справляється. Повтори на кількох рівнях перемножуються.
  • Повтори подовжують відповідь: три спроби з паузами 2, 4 і 8 с разом із тайм-аутами легко перевищують терпіння користувача. Загальний тайм-аут це обмежує.
  • Розімкнений circuit breaker — швидка відмова, яку треба обробити: відповідь 503, fallback або повідомлення в чергу на потім.

Компроміси

  • Де повторювати. На одному рівні — зазвичай найближче до виклику. Сервіс посередині ланцюжка помилку прокидає, а не повторює.
  • Hedging. AddStandardHedgingHandler надсилає паралельну копію повільного запиту й бере першу відповідь. Це зменшує хвіст затримки ціною зайвого навантаження, і лише для ідемпотентних операцій.
  • Retry-After. Стандартний повтор враховує цей заголовок у відповідях 429 і 503: сусід сам каже, коли повертатися.

Як перевірити в продакшені

  • Polly публікує метрику resilience.polly.strategy.events і логи подій: повтори, спрацювання тайм-аутів, розмикання breaker.
  • Порівнюють кількість вихідних спроб із кількістю логічних викликів: їхнє відношення показує частку повторів. Зростання цієї частки — перша ознака проблем у сусіда.

Підсумок: стійкість — це бюджет часу, обмежені повтори на одному рівні й швидка відмова за хворого сусіда; без ідемпотентності повтори перетворюються на дублі.

Приклад
Microsoft.Extensions.Http.Resilience
using Microsoft.Extensions.Http.Resilience;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<ErpClient>(c => c.BaseAddress = new Uri("https://erp.example.com"))
.AddStandardResilienceHandler(options =>
{
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5); // одна спроба
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(20); // усі спроби разом
options.Retry.MaxRetryAttempts = 3; // експонента й jitter увімкнені за замовчуванням
options.Retry.DisableForUnsafeHttpMethods(); // POST, PUT, PATCH, DELETE не повторюємо
});
var app = builder.Build();
app.MapGet("/orders/{id}/erp-status", (string id, ErpClient erp, CancellationToken ct) =>
erp.GetOrderStatusAsync(id, ct));
app.Run();
public class ErpClient(HttpClient http)
{
public Task<string> GetOrderStatusAsync(string id, CancellationToken ct) =>
http.GetStringAsync($"/orders/{id}/status", ct);
}
Polly v8 для виклику не через HttpClient
Polly.Core
using Polly;
using Polly.CircuitBreaker;
using Polly.Retry;
var pipeline = new ResiliencePipelineBuilder()
.AddTimeout(TimeSpan.FromSeconds(30)) // зовнішня: бюджет на всі спроби
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential, // 1 с, 2 с, 4 с
UseJitter = true, // розкид, щоб клієнти не били хором
Delay = TimeSpan.FromSeconds(1)
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5, MinimumThroughput = 10,
SamplingDuration = TimeSpan.FromSeconds(30), BreakDuration = TimeSpan.FromSeconds(15)
})
.AddTimeout(TimeSpan.FromSeconds(5)) // внутрішня: одна спроба
.Build();
var status = await pipeline.ExecuteAsync(async ct => await GetInvoiceStatusAsync("INV-1001", ct));
Console.WriteLine(status); // Accepted
static async Task<string> GetInvoiceStatusAsync(string number, CancellationToken ct)
{
await Task.Delay(100, ct); // тут виклик SOAP-сервісу або SFTP
return "Accepted";
}
Middle+

Що таке ідемпотентність і як забезпечити її в інтеграціях?

Section titled “Що таке ідемпотентність і як забезпечити її в інтеграціях?”
Відповідь

Що це

Ідемпотентна операція — та, яку можна виконати кілька разів із тим самим результатом, що й один раз.

Операція Ідемпотентна?
Встановити статус «Відвантажено» так
Видалити замовлення 42 так
Додати 10 до залишку ні
Створити замовлення без ключа ні: кожен повтор — нове замовлення

Навіщо

Мережа ненадійна. Якщо відповідь загубилася, відправник не знає, чи виконалася операція, і повторює її. Брокер після збою теж доставить повідомлення ще раз.

Як користуватися

Отримувач зберігає id оброблених повідомлень. Якщо id уже є — повідомлення підтверджують і пропускають.

Типова помилка

Вважати, що повторів «не буває». Вони трапляються за кожного збою, а дубль накладної чи списання знаходять лише під час звірки.

Як влаштовано

Спосіб Як працює Приклад
Природна ідемпотентність операція задає стан, а не змінює його UPDATE ... SET status = 'Shipped', upsert за номером документа
Дедуплікація за id повідомлення таблиця оброблених id з унікальним ключем обробник повідомлень із брокера
Ключ ідемпотентності клієнт надсилає ключ, сервер зберігає результат першої обробки POST створення платежу

Як працює Idempotency-Key в HTTP API, розібрано в темі Web API.

Коли що обирати

  • Можна переписати операцію як «встановити» — це найдешевший шлях, сховище не потрібне.
  • Обробник повідомлень — дедуплікація за id повідомлення або бізнес-ключем документа.
  • Створення сутності чи платіж через API — ключ ідемпотентності й збережена відповідь.

Пастки

  • Перевірка й запис окремо. «Перевірив, що ключа немає, потім обробив» — два паралельні повтори пройдуть обидва. Потрібна атомарна вставка ключа з унікальним індексом.
  • Позначка до ефекту. Якщо спочатку записати «оброблено», а потім упасти, повідомлення загубиться. Позначка й ефект — в одній транзакції.
  • Ключ без вмісту. Той самий ключ з іншим тілом запиту — помилка клієнта, а не повтор. Сервер порівнює хеш тіла й відповідає помилкою.

Правило

Кожен обробник вхідних даних проєктують так, ніби кожне повідомлення прийде двічі.

Під капотом

  • Надійна дедуплікація — унікальний індекс: INSERT ключа в тій самій транзакції, що й бізнес-зміна. Конфлікт ключа — ознака дубля, транзакцію відкочують, а повідомлення підтверджують.
  • Для HTTP-ключа зберігають стан «в обробці» й готову відповідь. Повтор під час обробки отримує 409, після — збережену відповідь.
  • Таблиця оброблених повідомлень в отримувача — частина патерну inbox; разом з outbox його розбирають у темі «Черги й Kafka».

Ціна

  • Сховище ключів зростає з трафіком: потрібен строк зберігання (TTL) і регулярне очищення.
  • Строк зберігання ключа має бути більшим за вікно повторів відправника. Якщо партнер може перезавантажити файл через тиждень, ключ, видалений через добу, не врятує.
  • Ключ у тій самій базі, що й дані, зручний для транзакції, але навантажує її за великого трафіку.

Компроміси

  • Ключ з id повідомлення чи з бізнесу. Id повідомлення ловить технічні повтори, але не повторне надсилання того самого документа з новим id. Бізнес-ключ — номер документа й відправник — ловить обидва.
  • Порядок. Дедуплікація не рятує від старого повідомлення, що прийшло після нового. Зміни стану несуть версію, і отримувач застосовує лише новішу.
  • Зовнішні ефекти. Надсилання SMS чи платіж не відкотити транзакцією. Ключ передають провайдеру, якщо він його підтримує, або ведуть журнал надісланого й звіряють його.

Як перевірити в продакшені

  • Рахують спрацювання дедуплікації: зростання частки дублів показує проблеми у відправника чи брокера.
  • Періодичний запит шукає дублі за бізнес-ключем — номером документа, зовнішнім id — у цільових таблицях.

Підсумок: ідемпотентність тримається на атомарному унікальному ключі, розумному строку його зберігання й версіях проти неправильного порядку.

Приклад
var handler = new PriceHandler();
var msg = new PriceChanged("m-1", "SKU-1", 49.90m, Version: 7);
Console.WriteLine(handler.Handle(msg)); // Applied
Console.WriteLine(handler.Handle(msg)); // Duplicate: те саме повідомлення прийшло повторно
Console.WriteLine(handler.Handle(msg with { MessageId = "m-0", Price = 45m, Version = 6 })); // Stale
public record PriceChanged(string MessageId, string Sku, decimal Price, long Version);
public class PriceHandler
{
// У продакшені — таблиці з унікальним ключем, що оновлюються в одній транзакції
private readonly HashSet<string> _processed = [];
private readonly Dictionary<string, (decimal Price, long Version)> _prices = [];
public string Handle(PriceChanged m)
{
if (!_processed.Add(m.MessageId)) return "Duplicate"; // дедуплікація за id
if (_prices.TryGetValue(m.Sku, out var current) && current.Version >= m.Version)
return "Stale"; // стара версія прийшла після нової
_prices[m.Sku] = (m.Price, m.Version); // «встановити», а не «додати»
return "Applied";
}
}
Senior+

Як інтегруватися із зовнішніми та legacy-системами (ERP, облікові системи, EDI-провайдери)?

Section titled “Як інтегруватися із зовнішніми та legacy-системами (ERP, облікові системи, EDI-провайдери)?”
Відповідь

Що це

  • Legacy- і зовнішні системи — ERP, облікові системи, системи партнерів. Їх не можна змінити, у них свої формати й обмеження.
  • EDI (electronic data interchange) — обмін стандартними електронними документами між компаніями: замовлення, повідомлення про відвантаження, рахунок. Зазвичай через EDI-провайдера.

Як користуватися

  1. Зробіть адаптер — окремий клас або сервіс, який єдиний знає формат зовнішньої системи.
  2. Адаптер перекладає чужі дані у ваші класи й назад.
  3. Кожен вхідний документ збережіть як є, до розбору.

Типова помилка

Протягнути чужу модель у свій код: поля DOC_NO, RCV_FLAG і коди ERP розповзаються всією системою, і заміна ERP перетворюється на переписування.

Як влаштовано

  • Anti-corruption layer (ACL). Шар між доменом і зовнішньою системою: перекладає моделі, коди, одиниці виміру й помилки. Домен говорить своєю мовою.
  • Транспорт. API або SOAP для онлайн-запитів; файли через SFTP або спільну папку для пакетів; AS2 — HTTP з підписом, шифруванням і квитанцією MDN, класика EDI.
  • Формати EDI. UN/EDIFACT — повідомлення ORDERS (замовлення), DESADV (повідомлення про відвантаження), RECADV (приймання), INVOIC (рахунок). Багато провайдерів працюють і з XML-форматами.

Коли що обирати

Ситуація Підхід
Одна-дві зовнішні системи адаптер точка-точка
Багато партнерів з однаковими документами провайдер EDI й одна внутрішня модель документа
Довідники й великі обсяги раз на добу пакетне завантаження файлів через staging

Пастки

  • Немає квитанції — немає доставки. Файл, покладений на SFTP, ще не оброблений. Чекають підтвердження: MDN, CONTRL, відповідний статус.
  • Повторне завантаження. Той самий файл прийшов двічі — завантаження роблять ідемпотентним за номером документа й відправником.
  • Тихі помилки розбору. Документ, який не розібрався, має потрапити в чергу помилок з алертом, а не в лог.

Правило

Сире — зберегти, перекласти — в адаптері, стан — відстежувати, розбіжності — ловити звіркою.

Під капотом

  • Кожен документ — скінченний автомат: отримано, розібрано, завантажено, підтверджено або помилка. Статус і кореляційний id зберігають у журналі обміну.
  • Вхідний файл зберігають в архів до розбору. Після виправлення мапінгу документ переобробляють з архіву, не просячи партнера надіслати його знову.
  • Звірка (reconciliation) — періодичне порівняння двох сторін: кількість, суми й номери документів за період. Вона ловить те, що пропустили квитанції.

Ціна

  • Точка-точка: n систем, пов’язаних кожна з кожною, — до n(n−1)/2 інтеграцій, і в кожної свій мапінг.
  • Шина скорочує кількість зв’язків до одного адаптера на систему, але додає центральний компонент, його ліцензію, окрему команду й єдину точку відмови.
  • Канонічна модель зменшує кількість мапінгів, але її зміна зачіпає всіх учасників.

Компроміси

  • ESB чи «розумні кінці, дурні труби». BizTalk — хаб з адаптерами, маршрутизацією, оркестраціями й вбудованою підтримкою EDI та AS2. Сучасний підхід: брокер лише доставляє, а логіка перекладу живе в невеликих сервісах-адаптерах.
  • Свій EDI чи провайдер. Провайдер бере на себе транспорт, формати конкретних мереж і квитанції. Ви платите залежністю й форматом провайдера.
  • Поступова заміна legacy — тема «Архітектурні стилі».

Як перевірити в продакшені

  • Алерт на документи без квитанції довше за SLA і на зростання черги помилок розбору.
  • Щоденна звірка зі звітом розбіжностей: надіслано й отримано, суми за рахунками, документи без відповіді.

Підсумок: надійна інтеграція з legacy тримається на ізоляції моделі, журналі статусів, архіві для повторної обробки й звірці, а шина — компроміс між кількістю зв’язків і централізацією.

Приклад
using System.Globalization;
var row = new ErpShipmentRow("0000123456", "20261009", "Y", "4820000000017;12|4820000000024;6");
var delivery = ErpShipmentTranslator.ToDomain(row);
Console.WriteLine($"{delivery.Number}: {delivery.Lines.Count} lines, received={delivery.IsReceived}");
// 123456: 2 lines, received=True
// Модель зовнішньої системи — як є, з усіма її особливостями
public record ErpShipmentRow(string DocNo, string DocDate, string RcvFlag, string Lines);
// Своя модель
public record Delivery(long Number, DateOnly Date, bool IsReceived, IReadOnlyList<DeliveryLine> Lines);
public record DeliveryLine(string Gtin, int Quantity);
// Anti-corruption layer: переклад в одному місці, домен не знає про формати ERP
public static class ErpShipmentTranslator
{
public static Delivery ToDomain(ErpShipmentRow row) => new(
long.Parse(row.DocNo, CultureInfo.InvariantCulture),
DateOnly.ParseExact(row.DocDate, "yyyyMMdd", CultureInfo.InvariantCulture),
row.RcvFlag == "Y",
row.Lines.Split('|')
.Select(line => line.Split(';'))
.Select(p => new DeliveryLine(p[0], int.Parse(p[1], CultureInfo.InvariantCulture)))
.ToList());
}
Звірка: документи без квитанції
-- Надіслані документи, на які за 4 години не прийшла квитанція
SELECT d.doc_number, d.partner_id, d.sent_at
FROM edi_outbound AS d
LEFT JOIN edi_ack AS a ON a.doc_id = d.id
WHERE a.doc_id IS NULL
AND d.sent_at < now() - interval '4 hours'
ORDER BY d.sent_at;