Стилі інтеграції
Як системи обмінюються даними: синхронно чи через повідомлення й файли, яким протоколом, як пережити збої сусіда й не створити дублів і як підключити ERP, облікові системи та EDI-провайдерів. На співбесіді тут перевіряють не знання API, а вміння пояснити, чим платиш за кожен вибір.
Перед цією темою: Web API.
Чим синхронна інтеграція відрізняється від асинхронної і коли яку обрати?
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 Acceptedapp.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); }}- Чому спільна база — погана інтеграція? — Схема стає контрактом із сусідом: її не можна змінювати, а бізнес-правила власника даних обходяться.
- Що робити, якщо операція триває хвилини, а клієнт викликає її по HTTP? — Асинхронний запит-відповідь: 202 Accepted, адреса статусу й опитування або callback.
- Як доступність залежить від довжини синхронного ланцюжка? — Перемножується: кожна ланка знижує загальну доступність.
- Чим небезпечний файловий обмін? — Недописані файли, повторне завантаження того самого файлу й загублені файли без квитанції.
- Асинхронна інтеграція завжди краща? — Ні: вона додає затримку, дублі й складність налагодження; там, де відповідь потрібна одразу, чесніший синхронний виклик.
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 — міст для перенесення, а не вибір для нового сервісу.
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; }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 });}Клієнт — фрагмент: завжди з дедлайном.
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");}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-конверт іде по HTTPSConsole.WriteLine(status);
((IClientChannel)client).Close();factory.Close();
// Контракт повторює WSDL; зазвичай його генерує dotnet-svcutil[ServiceContract]public interface IInvoiceService{ [OperationContract] Task<string> GetStatusAsync(string invoiceNumber);}- Чому gRPC не можна викликати з браузера напряму? — Браузер не дає керувати HTTP/2 і читати trailers; потрібен gRPC-Web або JSON transcoding.
- Які види викликів є в gRPC? — Unary, потік від сервера, потік від клієнта й двонаправлений.
- Що буде, якщо змінити номер поля в
.proto? — Старі клієнти прочитають поле не туди або втратять його: це ламальна зміна. - Як підняти старий WCF-сервіс на .NET 10? — CoreWCF на сервері; клієнти — пакети
System.ServiceModel.*і dotnet-svcutil. - Чому gRPC за L4-балансувальником навантажує один сервер? — Усі виклики мультиплексуються в одне довгоживуче HTTP/2-з’єднання.
Як зробити виклик зовнішнього сервісу стійким?
Section titled “Як зробити виклик зовнішнього сервісу стійким?”Що це
Тимчасовий збій — помилка, яка минає сама: обрив мережі, перезапуск сервісу, відповідь 503. Стійкий виклик переживає такі збої й не зависає, якщо сусід упав надовго.
Як користуватися
- Реєструйте клієнта через
IHttpClientFactory:builder.Services.AddHttpClient<ErpClient>(...). - Додайте пакет
Microsoft.Extensions.Http.Resilienceі викличте.AddStandardResilienceHandler(). - Передавайте
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. - Порівнюють кількість вихідних спроб із кількістю логічних викликів: їхнє відношення показує частку повторів. Зростання цієї частки — перша ознака проблем у сусіда.
Підсумок: стійкість — це бюджет часу, обмежені повтори на одному рівні й швидка відмова за хворого сусіда; без ідемпотентності повтори перетворюються на дублі.
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);}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";}- Чому повтор без jitter небезпечний? — Усі клієнти повторюють в одні й ті самі моменти й б’ють відновлений сервіс синхронною хвилею.
- Чим тайм-аут спроби відрізняється від загального тайм-ауту? — Перший обмежує одну спробу, другий — увесь виклик разом із повторами й паузами.
- Що робить circuit breaker у напіввідкритому стані? — Пропускає пробний виклик: успіх замикає коло, помилка знову розмикає.
- Чи можна повторювати POST? — Лише якщо операція ідемпотентна, наприклад із ключем ідемпотентності; інакше —
DisableForUnsafeHttpMethods. - Що замінило bulkhead у Polly v8? — Обмежувач конкурентності
AddConcurrencyLimiterз пакетаPolly.RateLimiting.
Що таке ідемпотентність і як забезпечити її в інтеграціях?
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)); // AppliedConsole.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"; }}- Які HTTP-методи ідемпотентні? — GET, HEAD, OPTIONS, TRACE, PUT і DELETE; POST і PATCH — ні.
- Чому позначку «оброблено» пишуть в одній транзакції зі зміною? — Інакше збій між ними дає або дубль, або загублене повідомлення.
- Скільки зберігати ключі ідемпотентності? — Довше за максимальне вікно повторів відправника, потім видаляти за TTL.
- Що робити, якщо повідомлення приходять не за порядком? — Передавати версію або час зміни й відкидати застарілі.
- Як зробити ідемпотентним надсилання SMS? — Ключ на боці провайдера або журнал надісланих з унікальним ключем і перевіркою перед надсиланням.
Як інтегруватися із зовнішніми та legacy-системами (ERP, облікові системи, EDI-провайдери)?
Section titled “Як інтегруватися із зовнішніми та legacy-системами (ERP, облікові системи, EDI-провайдери)?”Що це
- Legacy- і зовнішні системи — ERP, облікові системи, системи партнерів. Їх не можна змінити, у них свої формати й обмеження.
- EDI (electronic data interchange) — обмін стандартними електронними документами між компаніями: замовлення, повідомлення про відвантаження, рахунок. Зазвичай через EDI-провайдера.
Як користуватися
- Зробіть адаптер — окремий клас або сервіс, який єдиний знає формат зовнішньої системи.
- Адаптер перекладає чужі дані у ваші класи й назад.
- Кожен вхідний документ збережіть як є, до розбору.
Типова помилка
Протягнути чужу модель у свій код: поля 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: переклад в одному місці, домен не знає про формати ERPpublic 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_atFROM edi_outbound AS dLEFT JOIN edi_ack AS a ON a.doc_id = d.idWHERE a.doc_id IS NULL AND d.sent_at < now() - interval '4 hours'ORDER BY d.sent_at;- Що таке anti-corruption layer і де його межі? — Шар перекладу між доменом і зовнішньою системою; за його межі не виходять чужі моделі, коди й помилки.
- Чим AS2 відрізняється від SFTP? — AS2 — HTTP з підписом, шифруванням і квитанцією MDN; SFTP — передавання файлів по SSH без підтвердження обробки.
- Як знайти «загублений» документ? — За журналом статусів і кореляційним id; системно — алертом на документи без квитанції й щоденною звіркою.
- Навіщо зберігати сирий вхідний файл? — Щоб переобробити його після виправлення мапінгу й розбирати суперечки з партнером.
- Коли потрібна шина, а коли досить точка-точка? — Шина окупається за багатьох систем і спільних документів; на двох-трьох зв’язках вона зайва.