Архітектурні стилі
Як розкладати код і сервіси: шари й чиста архітектура, моноліт проти мікросервісів, DDD, CQRS і event sourcing, перенесення legacy без великого переписування. На співбесіді тут перевіряють не визначення, а вміння назвати ціну кожного рішення й умови, за яких воно окуповується.
Перед цією темою: Впровадження залежностей.
Чим шарувата архітектура відрізняється від чистої (гексагональної, onion)?
Section titled “Чим шарувата архітектура відрізняється від чистої (гексагональної, onion)?”Що це
Шарувата архітектура, згори вниз; шар викликає лише нижній:
- Представлення — контролери.
- Бізнес-логіка — сервіси.
- Доступ до даних — репозиторії, EF Core.
Чиста архітектура, від центру назовні; зовнішній шар залежить від внутрішнього:
- Domain — сутності й правила, без залежностей.
- Application — сценарії та інтерфейси-порти:
IOrderRepository. - Infrastructure — реалізації портів: EF Core, SMTP.
- Web API — викликає сценарії та зв’язує порти з реалізаціями.
Де живе інтерфейс репозиторію
У шаруватій — у шарі даних, поруч із реалізацією. У чистій — у ядрі, поруч із тим, хто його викликає.
Типова помилка
Розкласти код по папках Domain та Infrastructure, але залишити в домені атрибути EF Core. Залежність однаково спрямована назовні.
Як влаштовано
- Порт — інтерфейс, який оголошує ядро. Вхідний порт — сценарій, який викликають ззовні. Вихідний — те, що потрібно ядру: сховище, пошта, зовнішній API.
- Адаптер — реалізація порту під технологію. Контролер — вхідний адаптер, репозиторій на EF Core — вихідний.
- Напрямок. Посилання між проєктами йдуть усередину, а виклики під час роботи — в обидва боки: ядро викликає репозиторій через інтерфейс. Зв’язує все composition root у
Program.cs.
Коли що обирати
| Ситуація | Стиль |
|---|---|
| CRUD, мало правил | шарувата або вертикальні зрізи за фічами |
| Складні правила, багато зовнішніх систем | clean або hexagonal |
| Інфраструктура змінюватиметься | порти й адаптери на цій межі |
Пастки
- Протікаючий порт. Репозиторій повертає
IQueryable<T>— ядро знову залежить від EF Core і від того, які запити він уміє перекласти в SQL. - Інтерфейс на кожен клас. Порт потрібен на межі із зовнішнім світом, а не між двома класами домену.
- Generic-репозиторій поверх EF Core.
DbContextуже реалізує unit of work, аDbSet<T>— репозиторій; обгорткаIRepository<T>зGetAllнічого не додає.
Правило
Інтерфейс оголошує той, хто його використовує; посилання між проєктами спрямовані до домену.
Під капотом
- Hexagonal — Алістер Кокберн, 2005: у застосунку немає «верху» й «низу», є ведучі порти (UI, тести) та ведені (база, черги).
- Onion — Джеффрі Палермо, 2008: концентричні шари, домен у центрі.
- Clean — Роберт Мартін, 2012: Dependency Rule, залежності в коді вказують лише всередину.
- Анемічна модель ортогональна стилю. Фаулер описує багату модель предметної області як частину доменного шару шаруватої архітектури.
Ціна
- Мапінг: доменна сутність, сутність EF Core і DTO API — три моделі. Компроміс Microsoft в eShopOnWeb: доменні сутності мапляться через Fluent API в Infrastructure, без атрибутів у домені.
- Опосередкованість: шлях від ендпоінта до SQL проходить через кілька проєктів, налагодження й навігація повільніші.
Компроміси
- Невеликий сервіс без складних правил дешевше писати вертикальними зрізами: ендпоінт, обробник і запит до
DbContextпоруч. - Окремий проєкт Domain без посилань на пакети коштує майже нічого й дає головне — компілятор не пустить EF Core у домен.
Як перевірити в продакшені
- У
Domain.csprojнемаєPackageReferenceна EF Core та ASP.NET Core. - Тести архітектури (NetArchTest, ArchUnitNET) у CI падають, якщо домен посилається на інфраструктуру.
Підсумок: суть чистої архітектури — напрямок залежностей до домену; кількість шарів і форма моделі — окремі рішення зі своєю ціною.
using Shop.Application;using Shop.Infrastructure;
// Composition root: порт зв'язується з адаптером поза ядромvar handler = new PlaceOrderHandler(new ConsoleOrderRepository());handler.Handle(42, 3); // Saved Order { Id = 42, Quantity = 3 }
namespace Shop.Domain{ // Ядро: не посилається ні на EF Core, ні на ASP.NET Core public record Order(int Id, int Quantity);}
namespace Shop.Application{ using Shop.Domain;
// Порт оголошує той, хто його використовує public interface IOrderRepository { void Save(Order order); }
public class PlaceOrderHandler(IOrderRepository repository) { public void Handle(int id, int quantity) => repository.Save(new Order(id, quantity)); }}
namespace Shop.Infrastructure{ using Shop.Application; using Shop.Domain;
// Адаптер реалізує порт; у реальному коді тут EF Core public class ConsoleOrderRepository : IOrderRepository { public void Save(Order order) => Console.WriteLine($"Saved {order}"); }}У рішенні кожен простір імен — окремий проєкт: Infrastructure посилається на Application, Application — на Domain, зворотних посилань немає.
- Чим hexagonal відрізняється від onion і clean? — По суті нічим: одна ідея «залежності до домену», різна термінологія й малюнок.
- Де має лежати інтерфейс репозиторію? — У ядрі, поруч із тим, хто його використовує; реалізація — в Infrastructure.
- Чи потрібен репозиторій поверх EF Core? —
DbContextуже unit of work, аDbSet<T>— репозиторій; свій порт виправданий, якщо він описує запити домену, а не повторюєDbSet<T>. - Анемічна модель — ознака шаруватої архітектури? — Ні, це властивість моделі; багатий домен буває в шарах, анемічний — у «чистому» рішенні.
- Як не дати домену залежати від інфраструктури? — Окремий проєкт без посилань на пакети й тест архітектури в CI.
Моноліт, модульний моноліт чи мікросервіси: що коли обрати?
Section titled “Моноліт, модульний моноліт чи мікросервіси: що коли обрати?”Що це
| Стиль | Як влаштований |
|---|---|
| Моноліт | один застосунок, одна база, один деплой |
| Модульний моноліт | один деплой, код поділено на модулі з межами |
| Мікросервіси | багато сервісів, у кожного своя база й свій деплой |
Навіщо мікросервіси
- Частини системи випускаються й масштабуються незалежно.
- Команди не чекають одна на одну: кожна відповідає за свої сервіси.
Чим платять
Виклик сусіднього модуля перетворюється на мережевий запит. Він може впасти, зависнути або повернути застарілі дані, а однієї транзакції на два сервіси вже немає.
Типова помилка
Ділити невеликий застосунок однієї команди на мікросервіси «на виріст». Виходить та сама система, лише повільніша й складніша в налагодженні.
Як влаштовано
Модульний моноліт:
- Модуль — вертикальна частина за бізнес-можливістю: замовлення, склад, оплата. Не технічний шар.
- Назовні модуль відкриває лише контракт: інтерфейс, DTO, події. Реалізація —
internalв окремій збірці. - Дані модуля — у своїй схемі БД; чужі таблиці не читають і не джойнять.
- Межі перевіряють тести архітектури, а не домовленості.
Коли що обирати
| Ознака | Модульний моноліт | Мікросервіси |
|---|---|---|
| Деплой | один на все | незалежний у сервісу |
| Виклик сусіда | виклик методу | мережа: тайм-аути, повтори |
| Узгодженість | одна транзакція БД | eventual consistency |
| Масштабування | увесь застосунок | окремий сервіс |
| Налагодження | один процес | розподілене трасування |
Пастки
- Спільна база в сервісів. Сервіси пов’язані схемою: змінити таблицю — означає випустити всіх її читачів.
- Межі за технікою. Сервіс «валідації» або сервіс «доступу до БД» бере участь у кожному запиті.
- Синхронні ланцюжки. A чекає B, B чекає C: затримки додаються, а відмова будь-якого валить увесь ланцюжок.
Правило
Сервіс виділяють, коли в частини системи своя причина: навантаження, цикл релізів, команда, вимоги до надійності чи безпеки.
Ціна
- Мережа ненадійна: потрібні тайм-аути, повтори, ідемпотентність.
- Доступність ланцюжка — добуток доступностей: три сервіси по 99,9% дають близько 99,7%.
- Узгодженість між базами — eventual: outbox, саги й компенсації (тема «Черги й Kafka»).
- Інфраструктура на кожен сервіс: конвеєр випуску, моніторинг, трасування, версії контрактів.
Компроміси
- Закон Конвея. Мелвін Конвей, 1968: структура системи копіює структуру комунікацій. Сервіси без своєї команди-власника розпадаються на розподілений моноліт; тому інколи спершу змінюють команди, а потім архітектуру.
- Monolith first. Фаулер радить починати з моноліту: правильні межі видно лише після того, як домен зрозумілий. Перенести межу між модулями — рефакторинг, між сервісами — міграція даних.
- Незалежний деплой працює, лише якщо контракти зворотно сумісні й версіонуються.
Як перевірити в продакшені
- Сервіси завжди випускаються разом — це розподілений моноліт.
- У трасуванні видно глибину синхронних ланцюжків на запит.
- Тести архітектури в CI ловлять звернення модуля до внутрішніх частин іншого.
Підсумок: мікросервіси — обмін простоти на незалежність команд; без реальної потреби в цій незалежності модульний моноліт дешевший і простіший у змінах.
using Orders;
var builder = WebApplication.CreateBuilder(args);
// Модуль реєструє себе сам; іншим модулям видно лише контрактbuilder.Services.AddOrdersModule();
var app = builder.Build();app.MapGet("/orders/{id:int}", (int id, IOrdersApi orders) => orders.GetStatus(id));app.Run(); // GET /orders/42 → "Order 42: shipped"
namespace Orders{ // Публічний контракт модуля public interface IOrdersApi { string GetStatus(int id); }
// У реальному рішенні модуль — окремий проєкт, // і internal закриває реалізацію від інших модулів internal sealed class OrdersService : IOrdersApi { public string GetStatus(int id) => $"Order {id}: shipped"; }
public static class OrdersModule { public static IServiceCollection AddOrdersModule(this IServiceCollection services) => services.AddScoped<IOrdersApi, OrdersService>(); }}- Як перевірити межі модулів у моноліті? — Окремі збірки з
internal, своя схема БД на модуль і тести архітектури в CI. - Що таке розподілений моноліт? — Сервіси, які не можна випускати окремо: спільна база, синхронні ланцюжки, спільні релізи.
- Чи можуть мікросервіси ділити одну базу? — Як перехідний стан — так, але тоді схема стає спільним контрактом і незалежний деплой втрачається.
- Як закон Конвея впливає на архітектуру? — Межі сервісів повторюють межі команд; без власника сервіс розповзається.
- Коли виділяти модуль у сервіс? — Коли в нього своє навантаження, свій цикл релізів, своя команда або свої вимоги до надійності.
Що таке DDD і які його ідеї потрібні на практиці?
Section titled “Що таке DDD і які його ідеї потрібні на практиці?”Що це
DDD — підхід із книжки Еріка Еванса (2003): код описує бізнес тими самими словами й правилами, що й експерти.
Головні ідеї
- Єдина мова. Якщо склад каже «резерв» і «відвантаження», у коді ті самі
ReserveіShipment, а неUpdateStatus(3). - Обмежений контекст. Межа, де слово має одне значення. «Товар» на складі — місце й вага, у каталозі — опис і ціна.
- Value object. Значення без ідентичності:
Money,Address. Два об’єкти рівні, якщо рівні поля. - Агрегат. Група об’єктів, яку змінюють цілком через корінь:
Orderі його рядки.
Типова помилка
Вважати DDD набором папок Entities, ValueObjects, Repositories. Без єдиної мови й меж це просто структура проєкту.
Як влаштовано
| Рівень | Що вирішує | Інструменти |
|---|---|---|
| Стратегічний | як розрізати систему | піддомени, контексти, карта контекстів |
| Тактичний | як моделювати всередині | сутності, value objects, агрегати, події |
Агрегат:
- Корінь — єдина точка входу:
order.AddLine(...), а неorder.Lines.Add(...). - Інваріанти («сума не більша за кредитний ліміт») перевіряються в методах кореня.
- Репозиторій — на агрегат, а не на таблицю: агрегат завантажують і зберігають цілком.
- Доменна подія (
OrderSubmitted) повідомляє про факт, і інші агрегати реагують на неї у своїх транзакціях.
Пастки
- Величезний агрегат. «Клієнт з усіма замовленнями» довго завантажується й постійно ловить конфлікти конкурентності.
- Публічні сетери. Інваріанти розповзаються по сервісах, і модель стає анемічною.
- Одна модель на всю компанію. Єдиний
Customerдля продажів, доставки й бухгалтерії обростає полями, потрібними лише одному контексту.
Правило
Одна транзакція змінює один агрегат; інші агрегати — за ідентифікатором і через події.
Карта контекстів
Карта описує стосунки між контекстами й командами:
| Стосунок | Зміст |
|---|---|
| Customer/Supplier | постачальник враховує потреби споживача |
| Conformist | споживач приймає чужу модель як є |
| Anti-corruption layer | споживач перекладає чужу модель у свою |
| Open host service, published language | постачальник дає стабільний публічний протокол |
| Shared kernel | дві команди ділять частину моделі й змінюють її разом |
Ціна
- Моделювання потребує часу експертів; без їхньої участі виходить модель розробника, а не бізнесу.
- Value objects і закриті колекції потребують налаштування EF Core: owned types, complex types (EF Core 8+), мапінг полів.
- Узгодження агрегатів через події — eventual consistency. Інтеграційні події в інші сервіси надійно надсилають через outbox (тема «Черги й Kafka»).
Компроміси
- Еванс ділить піддомени на core, supporting і generic. Повний тактичний DDD — лише для core; generic, наприклад авторизацію, купують.
- Вернон (Effective Aggregate Design): маленькі агрегати, посилання за Id, eventual consistency за межею агрегату. Якщо бізнес вимагає негайної узгодженості двох агрегатів, це привід переглянути межі.
Як перевірити в продакшені
- Часті
DbUpdateConcurrencyExceptionна одному агрегаті — він завеликий або гарячий. - На рев’ю видно транзакцію, яка зберігає кілька агрегатів одразу.
Підсумок: головне в DDD — межі контекстів і маленькі агрегати з явними інваріантами; тактичні патерни застосовують лише там, де складність домену їх окуповує.
var order = new Order(creditLimit: new Money(100m, "UAH"));order.AddLine("SKU-1", new Money(60m, "UAH"));
try { order.AddLine("SKU-2", new Money(50m, "UAH")); }catch (InvalidOperationException ex) { Console.WriteLine(ex.Message); } // Credit limit exceeded
order.Submit();Console.WriteLine(order.Events[0]); // OrderSubmitted { OrderId = ... }Console.WriteLine(new Money(5m, "UAH") == new Money(5m, "UAH")); // True: рівність за значенням
// Value object: незмінний record без ідентичностіpublic record Money(decimal Amount, string Currency){ public Money Add(Money other) => other.Currency == Currency ? this with { Amount = Amount + other.Amount } : throw new InvalidOperationException("Currency mismatch");}
public record OrderSubmitted(Guid OrderId);
// Корінь агрегату: стан змінюється лише через методи, інваріанти всерединіpublic class Order(Money creditLimit){ private readonly List<Money> _lines = []; private readonly List<object> _events = [];
public Guid Id { get; } = Guid.NewGuid(); public IReadOnlyList<object> Events => _events;
public void AddLine(string sku, Money price) { var total = _lines.Aggregate(price, (sum, line) => sum.Add(line)); if (total.Amount > creditLimit.Amount) throw new InvalidOperationException("Credit limit exceeded"); _lines.Add(price); }
public void Submit() => _events.Add(new OrderSubmitted(Id)); // доменна подія}- Чим сутність відрізняється від value object? — Сутність має ідентичність, яка зберігається під час зміни полів; value object визначається лише значеннями.
- Чому агрегат має бути маленьким? — Він завантажується цілком і блокується цілком; великий агрегат — повільне завантаження й конфлікти конкурентності.
- Як пов’язати два агрегати? — Посиланням за Id і доменною подією; зміна другого — у своїй транзакції.
- Що таке обмежений контекст на практиці? — Межа модуля або сервісу зі своєю моделлю й своєю мовою.
- Чим доменна подія відрізняється від інтеграційної? — Доменна живе всередині контексту й процесу, інтеграційна — публічний контракт для інших сервісів.
Що таке CQRS і event sourcing і коли вони виправдані?
Section titled “Що таке CQRS і event sourcing і коли вони виправдані?”Що це
- Команда змінює стан і майже нічого не повертає:
PlaceOrder. - Запит повертає дані й нічого не змінює:
GetOrderSummary. - CQRS — у команд і запитів різні моделі: для запису — доменна модель із правилами, для читання — пласкі DTO під екран.
- Event sourcing — замість рядка «баланс = 70» зберігаються події
Deposited 100іWithdrawn 30; баланс отримують, програючи їх.
Навіщо
- Читання не тягне складну модель запису й пишеться простими запитами.
- Event sourcing зберігає історію кожної зміни.
Типова помилка
Думати, що CQRS вимагає двох баз і черги. Найпростіший CQRS — різні класи для команд і запитів над однією базою.
Як влаштовано
| Варіант | Читання | Узгодженість |
|---|---|---|
| Одна база, різні моделі | проєкції в DTO, AsNoTracking, Dapper |
одразу |
| Окрема модель читання | свої таблиці чи база, оновлюються за подіями | eventual |
| Event sourcing | проєкції з потоку подій | eventual |
Event sourcing покроково:
- Команда завантажує потік подій агрегату й відновлює його стан.
- Агрегат перевіряє інваріанти й створює нові події.
- Події дописуються у сховище подій з перевіркою очікуваної версії потоку — це оптимістична конкурентність.
- Проєкції читають нові події й оновлюють моделі читання.
Пастки
- Read-your-writes. Користувач зберіг і не бачить змін у списку. Допомагають відповідь із самої команди, очікування потрібної версії проєкції або оптимістичний UI.
- CRUD-події.
OrderUpdatedз усіма полями втрачає зміст; події називають бізнес-фактами в минулому часі:OrderShipped. - ES на всю систему. Довідники й налаштування в подіях лише ускладнюють код.
Правило
Починайте з CQRS в одній базі. Окрема модель читання — за реальної різниці навантаження або форми даних, ES — коли історія потрібна бізнесу.
Під капотом
- Сховище подій — журнал потоків лише на дозапис: KurrentDB (колишня EventStoreDB), Marten поверх PostgreSQL або своя таблиця з унікальним ключем (потік, версія).
- Снапшот — стан на версії N; агрегат завантажується з нього плюс хвіст подій. Потреба в снапшотах часто означає, що потік задовгий і його варто закривати за періодами.
- Нова модель читання будується програванням усіх подій від початку; на великих обсягах це години.
Ціна
- Версіонування. Події не змінюють заднім числом. Нові поля додають необов’язковими; несумісна зміна — новий тип події або upcasting: стару подію під час читання перетворюють на нову.
- Персональні дані. Видалення на вимогу закону конфліктує з незмінним журналом; такі дані зберігають поза подіями або шифрують ключем на людину й видаляють ключ.
Компроміси
- MediatR не дорівнює CQRS. CQRS — про окремі моделі, MediatR — патерн Mediator: він доставляє повідомлення обробнику всередині процесу. CQRS роблять і звичайними класами-обробниками.
- Ліцензія MediatR. 12.5.0 — остання версія під Apache 2.0. З 13.0 (2 липня 2025) — подвійна ліцензія RPL 1.5 і комерційна; Community-редакція безкоштовна за виручки до 5 млн доларів на рік і поза продакшеном. Для нових проєктів це привід узяти свій тонкий диспетчер.
Як перевірити в продакшені
- Відставання проєкцій: позиція останньої обробленої події проти кінця журналу — метрика з алертом.
- Довжина потоків і час завантаження агрегату показують, де потрібні снапшоти.
- Часті конфлікти очікуваної версії — гарячий потік.
Підсумок: CQRS і ES — інструменти для конкретного контексту з явною вигодою; їхня ціна — eventual consistency, версіонування подій і експлуатація проєкцій.
// Потік подій рахунку — єдине джерело правдиList<object> stream = [new Deposited(100m), new Withdrawn(30m)];
var account = new Account(stream); // стан = програні подіїaccount.Withdraw(50m);stream.AddRange(account.Changes); // у реальності — дозапис з очікуваною версією
Console.WriteLine(account.Balance); // 20
// Проєкція: модель читання з тих самих подійvar history = stream.Select(e => e switch{ Deposited d => $"+{d.Amount}", Withdrawn w => $"-{w.Amount}", _ => "?"});Console.WriteLine(string.Join(" ", history)); // +100 -30 -50
public record Deposited(decimal Amount);public record Withdrawn(decimal Amount);
public class Account{ public decimal Balance { get; private set; } public List<object> Changes { get; } = [];
public Account(IEnumerable<object> history) { foreach (var e in history) Apply(e); }
public void Withdraw(decimal amount) { if (amount > Balance) throw new InvalidOperationException("Insufficient funds"); var e = new Withdrawn(amount); Apply(e); Changes.Add(e); }
private void Apply(object e) => Balance += e switch { Deposited d => d.Amount, Withdrawn w => -w.Amount, _ => 0m };}- Чи обов’язкова окрема база для CQRS? — Ні, перший рівень — різні моделі й запити над однією базою.
- Як користувач побачить свої зміни, якщо модель читання відстає? — Відповідь із команди, очікування версії проєкції або оптимістичний UI.
- Як змінити формат події, яку вже записано? — Не змінювати: нові поля необов’язкові, несумісне — новий тип події або upcasting під час читання.
- Навіщо снапшоти? — Щоб не програвати довгий потік під час кожного завантаження агрегату.
- MediatR — це CQRS? — Ні, це внутрішньопроцесний диспетчер повідомлень; з версії 13 ще й з комерційною ліцензією.
Як поступово мігрувати legacy-систему без великого переписування?
Section titled “Як поступово мігрувати legacy-систему без великого переписування?”Що це
Велике переписування — зробити нову систему з нуля й перемкнутися одразу. Ризик: роки без користі для бізнесу, а стара система тим часом продовжує змінюватися.
Поступова міграція (strangler fig, Мартін Фаулер, 2004) — нове росте навколо старого, доки старе не стане непотрібним.
Як користуватися
- Обрати невелику цінну функцію: звіт, один екран.
- Описати її поточну поведінку тестами.
- Реалізувати її в новому сервісі.
- Перемкнути виклики: у вебі — через проксі, у товстому клієнті — викликом API замість прямого SQL.
- Переконатися, що старим шляхом ніхто не користується, і видалити його.
Типова помилка
Змінювати поведінку одночасно з перенесенням. Спершу перенести як є, потім покращувати, інакше незрозуміло, що зламала міграція.
Як влаштовано
- Фасад. YARP перед ASP.NET: перенесені маршрути обслуговує ASP.NET Core, решту
MapForwarderпересилає в старий застосунок. Спільні сесію й автентифікацію дають System.Web adapters. - Anti-corruption layer. Шар перекладу між моделлю legacy і новою: новий код не знає про таблицю
T_ORD_HDRі прапорці'Y'/'N'. - Характеризаційні тести (Майкл Фезерс). Фіксують, що код робить зараз, а не що має робити: реальні вхідні дані проганяють через старий код і зберігають результат як еталон.
- Feature toggles. Перемикання на нову реалізацію за користувачем, філією або відсотком, з миттєвим відкотом. У .NET —
Microsoft.FeatureManagement.
Товстий клієнт
Функції настільного клієнта переносять у сервіси: клієнт викликає HTTP API замість SQL у базу. Зазвичай спершу переносять читання, потім запис, а стару реалізацію в клієнті вимикають прапорцем.
Пастки
- Синхронізація в обидва боки між старою й новою базою: конфлікти й нескінченні цикли оновлень.
- Новий код читає legacy-таблиці напряму. Стара модель протікає в нову.
- Вічні прапорці. Кожен прапорець подвоює кількість шляхів у коді; у прапорця має бути строк видалення.
Правило
Кожен крок оборотний, у кожної таблиці — один власник, що пише.
Дані в перехідний період
- Спільна база: новий сервіс читає й пише старі таблиці через ACL. Зміни схеми — лише сумісні (зміна схеми без простою).
- Володіння переходить по таблицях: записувач один, інша сторона читає через представлення, API або потік змін.
- Окрема база нового сервісу — коли старих записувачів не залишилося.
Ціна
- Дві системи працюють і підтримуються одночасно, виправлення інколи роблять в обох.
- Синхронізація даних і спільна автентифікація — окрема інфраструктура на весь час міграції.
Компроміси
- Паралельний запуск. Нова реалізація рахує поруч зі старою, результати порівнюються, а відповідь віддає стара. Дорого, але знімає ризик для розрахунків: цін, залишків, нарахувань.
- Branch by abstraction. Коли фасад у мережі поставити нікуди, абстракцію вводять усередині коду, перемикають реалізацію за нею й видаляють стару.
Як перевірити в продакшені: готово до вимкнення
- За логами проксі на старі маршрути немає запитів довше за повний бізнес-цикл, включно з місячними й річними звітами.
- Старих версій товстого клієнта немає в телеметрії.
- Дані перенесено й звірено, старі записувачі вимкнені.
- Прапорці прибрано, старий код і маршрути видалено.
Підсумок: міграція — керована послідовність оборотних кроків з одним власником даних і заздалегідь заданими критеріями вимкнення.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpForwarder();builder.Services.AddScoped<IOrderReport, NewOrderReport>();
var app = builder.Build();
// Перенесена функція: її обслуговує новий код на ASP.NET Coreapp.MapGet("/reports/orders", (IOrderReport report) => report.Build());
// Усе, що ще не перенесено, іде в старий застосунок на ASP.NETapp.MapForwarder("/{**catch-all}", "https://legacy.internal") .WithOrder(int.MaxValue) // найнижчий пріоритет: спершу свої маршрути .ShortCircuit(); // пересланий запит не йде через решту конвеєра
app.Run();
public interface IOrderReport { string Build(); }
public class NewOrderReport : IOrderReport{ public string Build() => "Orders report from ASP.NET Core";}Перенесення наступної функції — новий ендпоінт в ASP.NET Core: проксі сам перестане надсилати цей маршрут у старий застосунок.
- Чому не переписати все з нуля? — Роки без користі, стара система продовжує змінюватися, а забута поведінка спливає після перемикання.
- Що таке характеризаційний тест? — Тест, що фіксує фактичну поведінку legacy, навіть помилкову; страховка під час перенесення.
- Чи може новий сервіс працювати зі старою базою? — Як перехідний стан — так, через ACL і з одним власником, що пише, для кожної таблиці.
- Як відкотити перенесену функцію? — Вимкнути прапорець або повернути маршрут у проксі; тому старий шлях видаляють лише після критеріїв вимкнення.
- Коли можна вимикати стару частину? — Немає трафіку довше за бізнес-цикл, немає старих клієнтів, дані звірено, прапорці й код видалено.