Архитектурный паттерн описывает, как разделить приложение на крупные компоненты и как им разрешено общаться между собой. Например, многослойная архитектура запрещает нижнему слою вызывать верхний, а микросервисная запрещает сервису читать чужую базу данных.
- Что такое архитектурный паттерн и чем он отличается от паттерна проектирования
- Основные архитектурные паттерны в одной таблице
- Многослойная архитектура
- MVC, MVP и MVVM: паттерны для интерфейса
- Гексагональная и чистая архитектура
- Клиент-серверная архитектура, каналы и фильтры
- Микросервисная архитектура
- Событийно-ориентированная архитектура
- Паттерны распределённых систем
- CQRS
- Event Sourcing
- Sidecar
- Circuit Breaker
- Strangler Fig
- Когда эти паттерны лишние
- Как выбрать архитектурный паттерн
- Частые вопросы
- Чем архитектурный паттерн отличается от паттерна проектирования из GoF?
- MVC это архитектурный паттерн или паттерн проектирования?
- С чего начать новичку?
- Можно ли сочетать несколько паттернов в одном проекте?
Что такое архитектурный паттерн и чем он отличается от паттерна проектирования
Архитектурным паттерном называют повторно применимое решение задачи уровня всей системы. Он определяет границы между компонентами и то, какие зависимости между ними допустимы. Паттерны проектирования (порождающие, структурные, поведенческие) работают уровнем ниже, например решают, как организовать создание объекта или подменить реализацию. Абстрактную фабрику можно использовать в любом слое, но слои она не задаёт: это решает архитектурный паттерн.
Рядом ходит термин «архитектурный стиль»: семейство архитектур с общими ограничениями, например многослойная или микросервисная. Граница между стилем и паттерном размыта.
Основные архитектурные паттерны в одной таблице
| Паттерн | Что разделяет | Когда подходит | Чем платите |
|---|---|---|---|
| Многослойная архитектура | представление, бизнес-логику, доступ к данным | типовое приложение, требования ещё меняются | новая функция задевает несколько слоёв |
| MVC, MVP, MVVM | данные, отображение, обработку ввода | приложения с пользовательским интерфейсом | экран делится на две-три роли, каждая в своём классе или функции |
| Гексагональная и чистая архитектура | ядро и внешний мир: базу, интерфейс, тесты | логика живёт дольше фреймворка, нужны тесты без базы | каждая внешняя зависимость получает интерфейс и адаптер |
| Каналы и фильтры | обработку на независимые шаги | шаги можно переставлять и запускать отдельно | плохо ложится на схему «запрос-ответ» |
| Микросервисы | приложение на сервисы со своими данными | сложная предметная область, частые релизы, несколько команд | сеть, согласованность данных, зрелый DevOps |
| Событийная архитектура | производителей и потребителей событий | одни события нужны нескольким подсистемам, реальное время | порядок доставки, итоговая согласованность |
| CQRS | чтение и запись | чтений намного больше, чем записей | две модели вместо одной, при раздельных базах чтение отстаёт от записи |
Первые четыре строки обычно касаются устройства одного приложения, нижние три чаще встречаются в системах из нескольких сервисов.
Многослойная архитектура
Это классический архитектурный шаблон для корпоративных приложений. Код делят на три слоя: представление (обрабатывает HTTP-запросы и рисует страницу), слой бизнес-логики (проверки и расчёты) и доступ к данным (база данных, внешние API). Верхний слой может обращаться к нижнему, нижний к верхнему нет.
В закрытом варианте слой зовёт только соседа снизу. В открытом может перепрыгнуть через уровень. Закрытый держит зависимости в узде, но если средний слой лишь передаёт запрос дальше, вы получаете код, который ничего не делает, и лишние вызовы.
Ниже пример, проверенный на Python 3.14.3. Сервис скидок не знает, где хранятся заказы: ему передают объект с методами get и save. Интерфейс Repository называют портом, а классы, которые его реализуют, адаптерами. Это уже шаг к гексагональной архитектуре.
import sqlite3
from dataclasses import dataclass
from typing import Protocol
@dataclass
class Order: # предметная область: правила без SQL и HTTP
id: int
amount: int # сумма в копейках
def apply_discount(self, percent: int) -> None:
if not 0 < percent <= 50:
raise ValueError("скидка должна быть от 1 до 50 %")
self.amount = self.amount * (100 - percent) // 100
class Repository(Protocol): # порт: что сценарию нужно от хранилища
def get(self, pk: int) -> Order: ...
def save(self, item: Order) -> None: ...
class DiscountService: # слой бизнес-логики
def __init__(self, repo: Repository):
self.repo = repo
def give(self, pk: int, percent: int) -> int:
item = self.repo.get(pk)
item.apply_discount(percent)
self.repo.save(item)
return item.amount
class MemoryRepo: # адаптер для тестов
def __init__(self):
self.rows = {1: 120000}
def get(self, pk):
return Order(pk, self.rows[pk])
def save(self, item):
self.rows[item.id] = item.amount
class SqliteRepo: # адаптер для базы данных
def __init__(self, db):
self.db = db
def get(self, pk):
sql = "SELECT id, amount FROM orders WHERE id = ?"
return Order(*self.db.execute(sql, (pk,)).fetchone())
def save(self, item):
sql = "UPDATE orders SET amount = ? WHERE id = ?"
self.db.execute(sql, (item.amount, item.id))
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, amount INTEGER)")
db.execute("INSERT INTO orders VALUES (1, 120000)")
for repo in (MemoryRepo(), SqliteRepo(db)):
print(type(repo).__name__, DiscountService(repo).give(1, 10))
DiscountService(MemoryRepo()).give(1, 70)
Вывод (трассировка сокращена):
MemoryRepo 108000
SqliteRepo 108000
Traceback (most recent call last):
...
File "orders.py", line 13, in apply_discount
raise ValueError("скидка должна быть от 1 до 50 %")
ValueError: скидка должна быть от 1 до 50 %
DiscountService отработал одинаково с двумя хранилищами, и в нём не поменялась ни одна строка. Правило про скидку живёт в Order, поэтому ошибку бросает модель, а не SQL-код. В проде стоит проверить граничный случай: если заказа с таким номером нет, fetchone() вернёт None, и адаптер упадёт с TypeError: __main__.Order() argument after * must be an iterable, not NoneType. Такую проверку пишут в адаптере и пробрасывают наверх понятное исключение.
Когда слоёв становится тесно, лучше делить приложение сверху на модули по предметной области (например, заказы и оплата), а внутри каждого модуля оставлять слои. Три общие папки «контроллеры, сервисы, репозитории» на весь проект удобны, пока проект маленький.

MVC, MVP и MVVM: паттерны для интерфейса
MVC (модель, представление, контроллер) описал Трюгве Реенскауг в 1978 году, версию для библиотеки Smalltalk-80 сделали другие разработчики. Идея в разделении ответственности: объекты предметной области отделены от объектов, которые пользователь видит на экране. Модель хранит данные и правила, вид (представление) показывает их и обновляется, когда модель изменилась, контроллер принимает действия пользователя и решает, что с ними делать.
Консольный пример на том же Python 3.14.3. Вид здесь сделан двумя функциями, контроллер одной: архитектура не требует, чтобы каждый компонент был классом.
class TodoModel:
def __init__(self):
self.items, self.listeners = [], []
def add(self, text: str) -> None:
text = text.strip()
if not text:
raise ValueError("пустая задача")
self.items.append(text)
for notify in self.listeners:
notify(self.items)
def render(items): # вид: только вывод
print(f"Задач: {len(items)}")
for n, task in enumerate(items, 1):
print(f" {n}. {task}")
def show_error(message): # тоже вид
print(f"Ошибка: {message}")
def handle(model, user_input): # контроллер: принимает ввод
try:
model.add(user_input)
except ValueError as e:
show_error(e)
model = TodoModel()
model.listeners.append(render) # вид подписан на модель
for line in ["купить хлеб", " ", "починить тест"]:
handle(model, line)
Задач: 1
1. купить хлеб
Ошибка: пустая задача
Задач: 2
1. купить хлеб
2. починить тест
Строка из пробелов не попала в список: модель отвергла её, контроллер поймал исключение и попросил вид показать ошибку. Модель при этом ничего не знает ни о print, ни о контроллере, и её можно тестировать без экрана.
MVP появился в 1990-х. Действия пользователя виджеты передают презентеру, и он решает, как на них реагировать. Экран варианты MVP обновляют по-разному: от подписки на модель до варианта, где все обновления делает презентер. MVVM прочно устоялся в .NET, например в .NET MAUI. В нём представление знает о модели представления (ViewModel), та знает о модели, но в обратную сторону связей нет. ViewModel выставляет свойства и команды, к которым экран привязывается через связывание данных, поэтому её можно покрыть юнит-тестами без интерфейса.
| Кто обрабатывает ввод | Как обновляется экран | |
|---|---|---|
| MVC | контроллер | вид подписан на модель и перерисовывается сам |
| MVP | презентер | по-разному: подписка на модель или все обновления через презентер |
| MVVM | ViewModel через команды | связывание данных: экран привязан к свойствам ViewModel |

Выбирать между ними обычно не приходится: архитектурные паттерны MVC и MVVM задаёт фреймворк, и спорить с ним дороже, чем следовать его соглашениям.
Гексагональная и чистая архитектура
Алистер Кокберн в 2005 году описал гексагональную архитектуру, она же «порты и адаптеры». Цель: приложение одинаково можно запустить из интерфейса, из другой программы, из автотеста или пакетного скрипта, а разрабатывать и тестировать отдельно от базы данных и устройств. Порт описывает разговор с внешним миром («сохранить заказ»), адаптер переводит его на язык конкретной технологии. В примере со скидкой Repository был портом, MemoryRepo и SqliteRepo адаптерами.
Роберт Мартин в 2012 году свёл похожие подходы (гексагональный, луковый) в чистую архитектуру. Четыре круга от центра: сущности, сценарии использования, адаптеры интерфейсов, фреймворки и драйверы. Главное правило одно: зависимости в исходном коде направлены только внутрь. Сущность не импортирует библиотеку ORM, сценарий не знает про HTTP.
Для CRUD-формы на пять полей такая обвязка избыточна. Она окупается, когда бизнес-логика сложная и переживёт смену фреймворка или базы.

Клиент-серверная архитектура, каналы и фильтры
Клиент-сервер делит программу на две роли: клиент отправляет запрос, сервер обрабатывает его и возвращает ответ, как браузер и веб-сервер. Если страница и API открываются с разных источников (отличается домен, схема или порт), браузер сверяет ответ с разрешениями сервера, и без них вы увидите ошибку CORS.
Каналы и фильтры делят обработку на независимые шаги. Фильтр выполняет одно действие, канал только передаёт его результат следующему и никакой логики не содержит. Учебный пример под рукой в терминале Linux: каждая команда конвейера ничего не знает о соседях. Возьмём тестовый журнал веб-сервера в формате combined:
192.168.1.10 - - [05/Oct/2026:10:00:01 +0300] "GET / HTTP/1.1" 200 5120 "-" "Mozilla/5.0"
192.168.1.11 - - [05/Oct/2026:10:00:03 +0300] "POST /login HTTP/1.1" 302 0 "-" "Mozilla/5.0"
192.168.1.10 - - [05/Oct/2026:10:00:07 +0300] "GET / HTTP/1.1" 200 5120 "-" "Mozilla/5.0"
192.168.1.12 - - [05/Oct/2026:10:00:09 +0300] "GET /about HTTP/1.1" 200 2048 "-" "Mozilla/5.0"
192.168.1.11 - - [05/Oct/2026:10:00:12 +0300] "GET / HTTP/1.1" 200 5120 "-" "Mozilla/5.0"
Адрес страницы стоит седьмым полем, если делить строку по пробелам:
cut -d' ' -f7 access.log | sort | uniq -c | sort -rn
3 /
1 /login
1 /about
Четыре фильтра посчитали, какие адреса чаще встречаются в журнале. Порядок здесь важен: uniq -c склеивает только соседние одинаковые строки, поэтому вход у него должен быть отсортирован. Без первого sort на этом журнале получится пять строк с единицами. Новый фильтр, например grep, можно вставить перед sort, и остальные команды этого не заметят. Тот же приём работает с процессами в Linux, когда вывод ps фильтруют через grep. Если результат нужно вернуть клиенту сразу, в ответ на тот же запрос, паттерн подходит плохо.
Микросервисная архитектура
Микросервисная архитектура делит приложение на небольшие автономные сервисы. Каждый отвечает за одну бизнес-возможность, хранит свои данные сам и общается с остальными через API. Сервис может писать и поддерживать одна небольшая команда, выкатывают его независимо.
Ограничения стиля жёсткие: сервис не лезет в чужую базу, общих схем данных нет. Взамен вы получаете независимые релизы и изоляцию отказов. Каждый сервис проще монолита, но система в целом сложнее: появляются сетевые задержки, поиск сервисов, версии API, которые нельзя ломать соседям, и мониторинг цепочек вызовов. Изменение данных в двух сервисах, скорее всего, не получится провести одной ACID-транзакцией, как в одной базе SQL Server: приходится мириться с итоговой согласованностью.
Не начинайте разработку с микросервисов. Пока предметная область плохо изучена, границы между сервисами легко провести неправильно, а расходы на сеть, развёртывание и мониторинг вы платите с первого дня. Модульный монолит, где каждый модуль устроен слоями, потом проще разделить по уже проверенным границам.

Событийно-ориентированная архитектура
В событийно-ориентированной архитектуре есть производители, потребители и канал между ними, обычно брокер сообщений. Производитель публикует «заказ оплачен» и не знает, кто его слушает, например склад или рассылка. Новый потребитель добавляется без правки производителя.
Доставка устроена по одной из двух моделей. В модели «издатель-подписчик» сообщение рассылается текущим подписчикам и не хранится, поэтому новый подписчик старых не увидит. В потоковой модели всё пишется в журнал, и клиент может подключиться позже и перечитать его с нужного места.
Этот подход стоит использовать, когда одни и те же события нужны нескольким подсистемам или нужна обработка почти в реальном времени. Если хватает обычного синхронного вызова или бизнес-операция требует строгой согласованности между сервисами, событийная модель добавит сложность без пользы. Даже там, где события оправданы, придётся решать, как гарантировать доставку и что делать, если сообщение пришло дважды или не по порядку. В каждое событие стоит класть идентификатор корреляции, иначе цепочку обработки не найти в логах. Внутри одной программы такая развязка знакома по браузерному JavaScript: кнопка, на которую повесили обработчики через addEventListener, не знает, сколько их и что они делают. Если же обработчик ждёт ответа сервера, это уже работа с одной асинхронной операцией через async/await, и развязки производителя и потребителя там нет.
Паттерны распределённых систем
Эти паттерны решают проблемы, которые появляются, когда части приложения общаются по сети или старую систему переносят на новую архитектуру.

CQRS
CQRS разделяет операции записи (команды) и чтения (запросы) на две модели данных. Команда меняет данные, запрос только читает и никогда ничего не меняет. Модели можно держать в одной базе данных или в разных, тогда чтение масштабируют отдельно. Паттерн полезен, когда чтений намного больше, чем записей, или когда у предметной области сложная модель и интерфейс ведёт пользователя по шагам. При раздельных базах читатель может увидеть данные с задержкой.
Event Sourcing
Вместо текущего состояния данных хранится вся последовательность событий в журнале, куда можно только дописывать. Текущее состояние собирают, проигрывая события по порядку. Ошибочное событие не правят: добавляют компенсирующее. Так получается полный аудит, например для журнала платежей. Цена высокая: меняется работа с хранением, конкурентным доступом и схемами событий, а уйти с этого подхода потом дорого.
Sidecar
Вспомогательный компонент запускается в отдельном процессе или контейнере рядом с приложением и живёт столько же, сколько оно, как коляска мотоцикла. В него выносят логирование, мониторинг или сетевой прокси.

Circuit Breaker
«Автоматический выключатель» стоит между приложением и удалённым сервисом и перестаёт пропускать вызовы, когда число сбоев за период превысило порог. У него три состояния:
- закрыт: вызовы проходят, сбои считаются;
- открыт: вызов сразу возвращает ошибку, сервис получает время на восстановление;
- полуоткрыт: проходит немного пробных вызовов, и по их результату выключатель закрывается или снова открывается.
Strangler Fig
Strangler Fig помогает постепенно заменить старую систему. Перед ней ставят фасад-прокси, который отправляет часть запросов в старый код, часть в новые сервисы. Функции переносят по одной, пока старая система не опустеет.
Когда эти паттерны лишние
| Паттерн | Не подойдёт, если |
|---|---|
| CQRS | бизнес-правила простые и приложению хватает операций создания, чтения, обновления и удаления (CRUD) |
| Event Sourcing | нужен простой CRUD без истории и аудита, или это прототип с коротким сроком жизни |
| Sidecar | приложение маленькое, или компонент нужно масштабировать отдельно от приложения |
| Circuit Breaker | обращения идут к локальным ресурсам вроде структур в памяти, или им пытаются заменить обработку исключений в бизнес-логике |
| Strangler Fig | запросы к старой системе нельзя перехватить, или систему проще заменить целиком |
Как выбрать архитектурный паттерн
- Опишите, что должна делать система, и требования, которые нельзя нарушить: время выхода, нагрузку, число команд, согласованность данных.
- Начните с самого простого стиля, который их выдерживает. Обычно это монолит со слоями или с модулями по предметной области.
- Для интерфейса используйте паттерн, который диктует фреймворк: MVC, если на нём построен ваш веб-фреймворк, MVVM в .NET MAUI.
- Изолируйте базу и внешние API за интерфейсами там, где логика сложная. Это дешёвый задел на будущее разделение.
- Переходите к микросервисам, событиям и CQRS, когда видите конкретную проблему: модуль нужно масштабировать отдельно, команды мешают друг другу на релизах, чтение душит запись.
Признак того, что архитектура слишком проста для предметной области: зависимостями никто не управляет и структура расползается. Такой код называют «большим комом грязи».

Частые вопросы
Чем архитектурный паттерн отличается от паттерна проектирования из GoF?
Масштабом. Архитектурный паттерн определяет, на какие крупные части делится вся система и как они связаны. Паттерны проектирования GoF (одиночка, фабричный метод, наблюдатель и другие) решают задачу на уровне нескольких классов и могут встречаться в любом слое.
MVC это архитектурный паттерн или паттерн проектирования?
Обычно его относят к архитектурным: он делит приложение на три роли. В самом MVC при этом используются паттерны проектирования: например, вид подписан на модель по схеме «Наблюдатель».
С чего начать новичку?
С многослойной архитектуры и MVC: их проще всего разобрать на собственном коде. Сделайте небольшой проект, где контроллер не содержит SQL, а бизнес-логика не знает про HTTP. Пример со скидкой выше подойдёт как основа: добавьте к нему обработчик HTTP на любом фреймворке и проверьте, что DiscountService по-прежнему ничего не импортирует из веб-слоя.
Можно ли сочетать несколько паттернов в одном проекте?
Да, так и делают. Микросервис может быть устроен слоями, его веб-часть построена по MVC, а с соседями он обменивается сообщениями через брокер.








