Как мы перестраивали e‑commerce‑платформу в production: от legacy‑системы к Next.js и Symfony

Несколько лет назад мы оказались в ситуации, знакомой многим компаниям с давно работающим e‑commerce‑проектом: система продолжала выполнять свои задачи, бизнес рос, а стоимость каждого следующего изменения постепенно увеличивалась. Каталог становился больше, появлялись новые интеграции, менялись требования к SEO, производительности и интерфейсу. Нужно было автоматизировать работу с поставщиками, связывать интернет‑магазин с внутренними системами и при этом продолжать развивать существующий функционал.
В какой‑то момент стало понятно, что очередным рефакторингом отдельных частей проблему уже не решить. Нужна была новая платформа. При этом перед нами не стояла задача написать красивый greenfield‑проект с нуля. Интернет‑магазин уже работал, принимал заказы и был частью ежедневных процессов компании. Поэтому новая система должна была появиться рядом с существующей, постепенно взять на себя её функции и в итоге заменить её без длительной остановки бизнеса.
Основной цикл разработки новой платформы занял больше двух лет. После запуска работа не закончилась: значительная часть наиболее интересных архитектурных решений появилась уже тогда, когда система начала жить под реальной production‑нагрузкой. Сейчас frontend платформы работает на Next.js, backend — на Symfony, перед ними находится Nginx, а frontend обслуживается несколькими независимыми Node.js‑процессами. За этой относительно простой схемой накопилось довольно много решений, компромиссов и ошибок.
В этом проекте я участвовал в формировании технической архитектуры платформы, постановке задач и координации работы разработчиков и подрядчиков, а также непосредственно занимался разбором production‑проблем — от работы API и базы данных до Nginx, кэширования и серверной инфраструктуры. В этой статье я хочу разобрать именно их — не как инструкцию «как правильно строить e‑commerce», а как опыт постепенной перестройки работающей системы: почему мы отделили frontend от backend, зачем нам понадобился SSR, как устроили взаимодействие через API, почему одного уровня кэширования оказалось недостаточно и что спустя несколько лет я спроектировал бы иначе.
С чего всё начиналось
Предыдущая версия платформы была построена на Laravel. Сама по себе технология не была причиной миграции. Проблема заключалась в том, как система развивалась на протяжении нескольких лет.
По мере роста проекта в ней накапливались зависимости между каталогом, представлением данных, бизнес‑логикой и интеграциями. Изменения, которые раньше можно было сделать локально, всё чаще затрагивали несколько частей приложения.
Особенно это стало заметно в нескольких областях:
обработка большого товарного каталога;
импорт и обновление данных поставщиков;
цены и остатки;
интеграции с внешними системами;
SEO и серверный рендеринг страниц;
производительность каталога и карточек товаров;
дальнейшее развитие интерфейса.
Для понимания масштаба: на момент написания статьи в основной базе платформы находится более 96 тысяч товарных записей, 451 категория и 957 брендов. При этом только таблица связей товаров со значениями характеристик содержит более 850 тысяч записей. Поэтому операции с каталогом и массовыми обновлениями данных для нас давно перестали быть задачами небольшого интернет‑магазина.

Но, пожалуй, главная проблема была организационной: старая система постепенно превращалась в точку, через которую проходило слишком много разных процессов. Например, обработка данных поставщиков — это отдельная задача со своей логикой: разные форматы прайс‑листов, сопоставление товаров, правила наценки, остатки и периодические изменения структуры исходных файлов. Чем больше подобной логики оказывается непосредственно внутри основного приложения, тем сложнее становится независимо развивать сам интернет‑магазин. Поэтому при проектировании следующей версии мы старались разделять ответственность компонентов.
В упрощённом виде новая архитектура постепенно пришла к следующей схеме:
┌─────────────┐
│ Browser │
└──────┬──────┘
│
▼
┌─────────────┐
│ Nginx │
└──────┬──────┘
│
▼
┌───────────────────┐
│ Next.js │
│ SSR / Frontend │
└─────────┬─────────┘
│
HTTP API
│
▼
┌───────────────────┐
│ Symfony │
│ Backend / Admin │
└─────────┬─────────┘
│
▼
┌─────────┐
│ MySQL │
└─────────┘На production frontend при этом представляет собой не один Node.js‑процесс. Сейчас запущено восемь экземпляров Next.js на портах 3001–3008, а Nginx распределяет запросы между ними:
upstream next_cluster {
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
server 127.0.0.1:3004;
server 127.0.0.1:3005;
server 127.0.0.1:3006;
server 127.0.0.1:3007;
server 127.0.0.1:3008;
keepalive 32;
}
Каждый экземпляр запускается как отдельный процесс через PM2. Это важная деталь, к которой я ещё вернусь: наличие нескольких процессов влияет не только на распределение нагрузки. Оно меняет поведение любого кэша, который хранится непосредственно в памяти Node.js.
Почему мы разделили frontend и backend
Одним из принципиальных решений стало разделение пользовательской части и backend. Symfony отвечает за серверную бизнес‑логику, данные и административную часть. Next.js отвечает за пользовательский интерфейс и формирование страниц сайта. Между ними находится API. На бумаге такое разделение выглядит очевидным. В реальном проекте оно создаёт дополнительную сложность: появляется ещё один сетевой уровень, отдельное кэширование, обработка ошибок API, необходимость контролировать время ответа нескольких компонентов.
Но для нас преимущества оказались важнее. Frontend можно развивать независимо от административной части. Backend не должен знать, каким именно образом пользовательская страница будет отрисована. Интеграции также можно отделять от представления данных. А главное — стало проще определять ответственность каждого компонента.
Это особенно важно для e‑commerce, где одна карточка товара может зависеть сразу от нескольких источников данных: основной информации о товаре, категории, характеристик, бренда, цены, наличия, связанных товаров и дополнительного контента. При SSR все эти зависимости становятся частью времени генерации страницы. И здесь мы столкнулись со следующей проблемой.
SSR решил одну проблему и создал другую
Для значительной части публичных страниц нам нужен серверный HTML. Это важно прежде всего для каталога, карточек товаров и поисковых систем. Поэтому в проекте активно используется getServerSideProps, хотя для части контента используются также getStaticProps и ISR. На уровне отдельной страницы SSR выглядит просто:
request
↓
Next.js
↓
API requests
↓
Symfony
↓
Database
↓
HTMLНо если каждое открытие страницы каждый раз проходит весь этот путь, стоимость SSR быстро становится заметной. Предположим, для формирования страницы нужно получить несколько независимых наборов данных. Если запрашивать их последовательно, суммарное время начинает складываться:
request A → wait
request B → wait
request C → wait
request D → waitПоэтому независимые операции мы старались запускать параллельно, а уже затем ожидать результаты. Но даже параллельные запросы не решают фундаментальную проблему: если две тысячи пользователей запрашивают одну и ту же публичную страницу, нет большого смысла две тысячи раз генерировать одинаковый HTML. Так в архитектуре появился следующий слой — кэширование на Nginx.
Nginx как первый уровень кэша
Для основного frontend используется proxy_cache. Упрощённо конфигурация выглядит так:
proxy_cache nextjs_zone;
proxy_cache_valid 200 302 15m;
proxy_cache_valid 404 1m;
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
proxy_cache_use_stale
error
timeout
invalid_header
updating
http_500
http_502
http_503
http_504;
proxy_cache_background_update on;Для успешного публичного ответа Nginx может хранить результат 15 минут. Это существенно меняет путь запроса при попадании в кэш:
Browser
↓
Nginx
↓
HIT
↓
cached HTMLЭто можно увидеть непосредственно на production. Первый запрос к главной странице после отсутствия готовой записи возвращает X-Cache-Status: MISS, а повторный запрос — уже X-Cache-Status: HIT. Для публичного HTML при этом используется max-age=900 и stale-while-revalidate=60.

MISS, следующий обслуживается Nginx‑кэшем с HIT.Next.js, Symfony и база данных для такого запроса вообще не нужны. Это особенно существенно при большом каталоге: значительная часть публичных запросов при попадании в Nginx‑кэш вообще не проходит полный путь через Next.js, Symfony и базу данных. На момент одного из наблюдений production‑сервера при работающем приложении общая загрузка CPU составляла около 9%, то есть более 90% процессорного времени оставалось свободным. При этом одновременно работали Node.js‑процессы frontend, PHP‑FPM, MySQL и Nginx.

Но здесь есть важная оговорка: кэшировать все страницы интернет‑магазина таким способом нельзя. Страница каталога и страница оформления заказа имеют совершенно разные требования к данным. Поэтому для приватных и пользовательских маршрутов — профиля, заказа, избранного, сравнения и других подобных страниц — общий frontend‑кэш обходится. Отдельно отключено proxy‑кэширование frontend API.
Для таких ответов используется политика вида:
Cache-Control: no-store, no-cache, must-revalidate, privateА для кэшируемого публичного HTML браузер может получать:
Cache-Control: public, max-age=900, stale-while-revalidate=60То есть уже здесь становится видно, что вопрос «кэшируем ли мы сайт?» сформулирован неправильно. Правильнее спрашивать: какие данные кэшируются, на каком уровне, на какой срок и кто отвечает за их актуальность? И именно этот вопрос позже оказался значительно сложнее первоначальной задачи ускорения SSR.
Когда один memory cache превратился в восемь
Следующим шагом стало кэширование некоторых данных непосредственно внутри Next.js. Для этого у нас используется довольно простой helper на базе обычного Map:
type CacheEntry<T> = {
value: T;
expires: number;
};
const memoryCache = new Map<string, CacheEntry<any>>();
const pendingRequests = new Map<string, Promise<any>>();
export async function remember<T>(
key: string,
ttl: number,
callback: () => Promise<T>
): Promise<T> {
const now = Date.now();
const cached = memoryCache.get(key);
if (cached && cached.expires > now) {
return cached.value;
}
const pending = pendingRequests.get(key);
if (pending) {
return pending;
}
const request = callback()
.then((value) => {
memoryCache.set(key, {
value,
expires: Date.now() + ttl,
});
pendingRequests.delete(key);
return value;
})
.catch((err) => {
pendingRequests.delete(key);
throw err;
});
pendingRequests.set(key, request);
return request;
}На первый взгляд здесь ничего необычного: проверяем TTL, при отсутствии актуального значения вызываем callback и сохраняем результат. Есть ещё pendingRequests. Если два запроса одновременно захотели получить отсутствующие в кэше данные с одинаковым ключом, второй не запускает ещё один callback, а ждёт уже выполняющийся Promise. Для одного Node.js‑процесса это работает вполне предсказуемо. Но у нас не один процесс. Напомню production‑схему:
Nginx
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
Next.js #1 Next.js #2 ... Next.js #8
│ │ │
▼ ▼ ▼
Map #1 Map #2 Map #8Каждый PM2-процесс имеет собственную память. Следовательно, memoryCache тоже существует отдельно в каждом из восьми процессов. Это означает, что в действительности у нас может существовать восемь разных копий одного и того же значения с разным временем появления и, соответственно, разным временем истечения TTL.
Например:
12:00 process #1 caches product:X
12:03 process #4 caches product:X
12:07 process #7 caches product:XЕсли исходные данные изменились в 12:10, эти три процесса не узнают об этом автоматически. Каждый продолжит использовать свою копию до истечения собственного TTL либо явной очистки. То же относится к pendingRequests. Он предотвращает дублирующиеся запросы только внутри конкретного процесса. Если одновременно один и тот же ключ отсутствует у next-1 и next-5, оба процесса независимо обратятся к callback.
Это один из примеров того, как решение, прекрасно работающее в рамках одного процесса, меняет свойства после горизонтального масштабирования. Очистка кэша тоже локальна. В helper для этого есть отдельная функция:
export function clearMemoryCache(key?: string) {
if (!key) {
memoryCache.clear();
pendingRequests.clear();
return;
}
memoryCache.delete(key);
pendingRequests.delete(key);
}Но она очищает память только того процесса, внутри которого была вызвана. Если очистить product:X в next-3, его копии в next-1, next-2, next-4 и остальных процессах продолжат существовать. Для данных, где допустима небольшая eventual consistency, это может быть приемлемым компромиссом. Для данных, которые должны измениться одновременно во всех экземплярах приложения, такой механизм уже недостаточен.
И здесь возникает естественный вопрос: почему бы просто не заменить всё на Redis? Потому что общий (shared) cache тоже имеет свою цену. Появляется ещё один сетевой компонент, сериализация данных, отдельная инфраструктура, мониторинг, политика eviction и новая точка отказа. Не каждому значению нужна строгая синхронизация между процессами.
Поэтому сегодня я бы не заменял все локальные Map на Redis автоматически. Я бы сначала разделил данные по требованиям к консистентности, а shared cache использовал только там, где независимые копии действительно создают проблему.
Два разных механизма против одной и той же проблемы
Интересно, что похожая задача решается ещё на одном уровне. В Nginx у нас включено:
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;Представим популярную страницу, срок жизни которой только что закончился. Без координации несколько одновременных запросов могут практически одновременно обнаружить MISS и пойти генерировать один и тот же ответ. proxy_cache_lock позволяет координировать эту работу на уровне HTTP‑кэша. В приложении похожую функцию выполняет:
const pendingRequests = new Map<string, Promise<any>>();Но это два совершенно разных механизма:
Nginx
└── proxy_cache_lock
└── координация генерации HTTP response
Next.js process
└── pendingRequests
└── координация получения application dataПричём второй механизм, как мы уже увидели, ограничен одним процессом. Это хороший пример того, почему после появления нескольких уровней кэша становится опасно воспринимать «кэш» как один компонент системы. У каждого уровня свой ключ, свой TTL, своя область видимости и свои правила инвалидации.
Кэшировать HIT легко. Важнее понимать стоимость MISS
При оптимизации кэша легко смотреть только на долю попаданий в кэш (cache hit ratio). Но для production‑системы не менее важен другой вопрос: что происходит, когда кэша нет?
Если MISS запускает дорогостоящую цепочку последовательных операций, высокий процент HIT просто скрывает проблему. Поэтому часть оптимизации SSR у нас связана не только с кэшированием результата, но и с самим путём формирования страницы. Если несколько данных независимы друг от друга, нет необходимости делать так:
const a = await getA();
const b = await getB();
const c = await getC();Это создаёт waterfall. Вместо этого независимые операции можно начать заранее:
const aPromise = getA();
const bPromise = getB();
const cPromise = getC();
const [a, b, c] = await Promise.all([
aPromise,
bPromise,
cPromise,
]);На реальных страницах зависимости, конечно, сложнее: некоторые запросы нельзя начать до получения результатов предыдущих. Но сам принцип оказался полезным: кэш не должен становиться оправданием медленного пути без кэша. Это особенно важно после рестарта приложения, очистки кэша или появления нового URL, когда система внезапно начинает получать гораздо больше MISS, чем обычно.
Самая сложная проблема — не скорость, а актуальность
Когда мы начинали добавлять кэширование, основной вопрос звучал примерно так:
На сколько это уменьшит количество работы при формировании страницы?
Со временем более важным стал другой:
Через сколько пользователь увидит изменение исходных данных?
Допустим, существует значение X. Один из Next.js‑процессов получил его и положил в кэш приложения. Затем Nginx сохранил HTML страницы, построенной с этим значением. После этого исходное значение изменилось на Y. Получается примерно такая цепочка:
T0 Next.js process A caches X
T5 Nginx caches HTML containing X
T10 source changes X → Y
T20 cached HTML may still contain X
...
Nginx cache expires
...
request reaches one of Next.js processesНо и после истечения Nginx‑кэша результат зависит от состояния кэша приложения конкретного процесса, которому достался запрос. Важно: это иллюстрация поведения архитектуры, а не конкретный production‑инцидент. Также нельзя просто сложить TTL всех уровней и сказать:
15 min Nginx + 30 min application cache = 45 min staleСистема так не работает. Кэши создаются в разное время, запрос может попасть в разные процессы, некоторые значения могут отсутствовать в одном процессе и присутствовать в другом, а разные URL могут иметь разные политики. Поэтому максимальная потенциальная задержка обновления определяется не простой суммой TTL, а взаимодействием нескольких временных состояний. Именно здесь кэширование перестаёт быть только оптимизацией производительности и становится частью модели консистентности приложения.
Интеграции усиливают проблему актуальности
Для e‑commerce это особенно заметно, потому что данные меняются не только через административную панель. Часть информации приходит из внешних систем. Например, обработка данных поставщиков у нас вынесена из основной платформы. В упрощённом виде поток выглядит так:
KOBOR DB
│
▼
export
│
▼
external supplier-data processing system
│
▼
processed CSV
│
▼
staging table
│
▼
set-based SQL updates
│
▼
productИмпорт не обновляет каждую карточку товара отдельным HTTP‑запросом. Сначала данные загружаются во временную рабочую таблицу quad_upd_price, после чего production‑таблица обновляется set‑based SQL‑запросами через JOIN. Сопоставление происходит по:
product.article = quad_upd_price.xml_idТаким образом обновляются, в частности, цена, остаток, количество и другие связанные значения.
С точки зрения базы данных это удобно: массовое обновление выполняется непосредственно на уровне SQL. Но для кэша возникает другой вопрос. База уже содержит новое значение. Как все остальные уровни узнают, что старое больше нельзя использовать?
External system
↓
MySQL ← данные уже новые
↓
Symfony
↓
Next.js Map ← здесь ещё может быть старое значение
↓
Nginx cache ← и здесь может быть старый HTML
↓
BrowserИменно поэтому TTL — это не полноценная стратегия инвалидации. Он всего лишь устанавливает предел жизни конкретной копии данных.
Что я спроектировал бы иначе сегодня
Эта архитектура появилась не одномоментно. Она развивалась вместе с проектом: сначала нужно было отделить frontend от backend, затем решить вопросы SSR и производительности, позже появились дополнительные уровни кэширования и новые интеграции. Поэтому некоторые решения сегодня я бы проектировал иначе. Не потому, что текущий подход обязательно неправильный. Скорее несколько лет эксплуатации изменили понимание того, где именно должна находиться сложность системы.
1. Явно определил бы владельца каждого кэша
Когда в системе появляется несколько уровней кэширования, я бы для каждого типа данных заранее фиксировал четыре вещи:
Что кэшируем?
Где кэшируем?
Кто инвалидирует?
Какая задержка обновления допустима?Например:
Данные | Возможный уровень | Требования |
|---|---|---|
JS/CSS со fingerprint | Browser / Nginx | Очень длинный TTL |
Публичный HTML | Nginx | Короткий TTL или явная инвалидация |
Навигация и справочные данные | Application cache | Допустима eventual consistency |
Цена и наличие | Осторожное кэширование | Высокие требования к актуальности |
Персональные данные | Не shared cache | Изоляция пользователя |
Главный вывод здесь для меня простой: TTL не должен быть единственным описанием политики кэширования. 15m ничего не говорит о том, что произойдёт после изменения товара, кто должен сбросить старое значение и сколько копий этого значения существует в системе.
2. Для важных изменений добавил бы явную инвалидацию
Сейчас значительная часть актуальности достигается естественным истечением TTL. Это просто и надёжно в том смысле, что устаревшая запись рано или поздно исчезнет сама. Но для некоторых событий ждать необязательно. Например:
product updated
│
├── invalidate application cache
│
└── invalidate affected page cacheПроблема заключается в определении всех затронутых страниц. Изменение одного товара может затронуть не только:
/products/product-xно и:
/catalog/category-a
/catalog/category-a/tag-b
/search
brand pages
homepage blocks
related productsПоэтому подход «при изменении товара удалить один URL из кэша» быстро перестаёт работать.
Я бы строил инвалидацию вокруг зависимостей данных, а не только вокруг URL. При этом не стал бы пытаться сделать абсолютно всё event‑driven. Чем сложнее граф инвалидации, тем легче получить ошибку, при которой нужный кэш не сбросился вообще. Для менее критичных данных короткий TTL зачастую остаётся более практичным решением.
3. Shared cache использовал бы выборочно
Очевидным кандидатом на замену локального Map кажется Redis. Он решил бы одну важную проблему: восемь Next.js‑процессов могли бы видеть одну и ту же запись.
┌── Next #1 ──┐
├── Next #2 ──┤
Nginx ───────┼── ... ├── Shared Cache
└── Next #8 ──┘Но я не считаю, что Redis автоматически должен заменить весь локальный кэш. Локальная память имеет очень полезное свойство: доступ к ней практически ничего не стоит с точки зрения архитектуры приложения. Нет сетевого обращения и отдельного инфраструктурного компонента. Поэтому я бы разделил данные. Там, где допустимы независимые копии и небольшой период рассинхронизации, оставил бы локальный кэш процесса. Там, где всем экземплярам приложения действительно нужно видеть одно состояние, рассматривал бы shared cache.
То есть вопрос звучал бы не:
Map или Redis?
А:
Какая модель консистентности нужна именно этим данным?
4. Сразу заложил бы наблюдаемость кэшей
Nginx уже позволяет увидеть состояние HTTP‑кэша через заголовок:
X-Cache-StatusЭто полезно при диагностике конкретного запроса. Но по мере роста количества кэшей хочется видеть систему целиком. Например:
cache hit / miss ratio
cache key
cache age
generation time on MISS
application cache size
evictions
invalidations
upstream response timeОсобенно интересно время генерации при MISS. Высокий hit ratio может создавать ощущение очень быстрой системы, пока однажды значительная часть кэша не станет холодной. Если HIT занимает миллисекунды, а MISS запускает длинную цепочку API‑ и SQL‑запросов, именно второй сценарий определяет устойчивость архитектуры. Поэтому сегодня я бы рассматривал поведение системы с «холодным» кэшем как отдельный объект мониторинга и нагрузочного тестирования.
Интеграции я по‑прежнему держал бы отдельно
Есть и решения, которые после нескольких лет эксплуатации я бы не стал принципиально менять. Одно из них — отделение специфической логики внешних систем от ядра e‑commerce‑платформы.
У поставщиков могут меняться:
форматы файлов;
названия колонок;
структура категорий;
правила наличия;
идентификаторы;
способы формирования цен.
Если вся эта логика живёт внутри основного приложения, каждое изменение внешнего источника становится изменением e‑commerce‑платформы.
В нашем случае обработка данных поставщиков вынесена в отдельную систему, а основная платформа получает уже нормализованный результат. Это создаёт дополнительную границу интеграции, которую тоже приходится поддерживать. Но эта граница одновременно защищает ядро системы от специфики каждого источника. Для меня это оказалось одним из наиболее полезных архитектурных принципов проекта:
Нестабильность внешней системы не должна без необходимости становиться нестабильностью ядра приложения.
Не все страницы должны иметь одинаковую стратегию рендеринга
Ещё один вывод касается Next.js. В большом e‑commerce‑проекте я бы не пытался выбрать один универсальный режим:
всё SSRили
всё staticРазные страницы имеют разные свойства. Карточка товара, информационная страница, личный кабинет, результаты поиска и страница оформления заказа отличаются не только содержимым, но и требованиями к актуальности, персонализации и стоимости генерации. В текущем проекте поэтому сосуществуют SSR, статическая генерация и ISR. Я считаю это не недостатком архитектуры, а следствием разных требований. Полезнее выбирать стратегию для класса страниц:
page type
↓
freshness requirements
↓
personalization
↓
generation cost
↓
rendering strategy
↓
cache policyА уже после этого определять TTL и остальные параметры.
Архитектура после запуска только начинается
Одна из вещей, которые сильнее всего изменились в моём отношении к подобным проектам, — само понятие «запуска». Во время разработки легко воспринимать production‑релиз как финальную точку большого проекта. Для архитектуры всё почти наоборот.
Только после запуска становится видно:
какие маршруты действительно горячие;
какие API‑вызовы оказываются дорогими;
где кэш приносит наибольшую пользу;
какие данные пользователи ожидают видеть немедленно;
какие внешние интеграции меняются чаще остальных;
какие первоначальные предположения о нагрузке были неверными;
какие компоненты оказались слишком тесно связаны.
Поэтому архитектура production‑системы — это не схема, нарисованная перед началом разработки. Это постоянно меняющийся набор компромиссов между производительностью, актуальностью, сложностью и стоимостью сопровождения.
Итоги
Когда мы начинали перестраивать платформу, основные задачи выглядели довольно традиционно: отделить новый frontend, организовать API, обеспечить серверный рендеринг, улучшить производительность и создать основу для дальнейшего развития.
Со временем оказалось, что самые интересные проблемы возникают на границах между компонентами — не в Next.js, Symfony, Nginx или MySQL по отдельности, а в их взаимодействии:
Browser
↓
Nginx
↓
8 × Next.js
↓
Symfony
↓
MySQL
↑
external integrationsДобавление кэша ускоряет систему, но одновременно добавляет новое состояние. Добавление ещё одного процесса повышает возможности масштабирования, но превращает локальную память в несколько независимых состояний. Вынос интеграции уменьшает связанность ядра, но создаёт новую границу, которую нужно контролировать. SSR решает задачу формирования серверного HTML, но делает стоимость генерации страницы частью производительности backend.
За несколько лет работы с этой платформой мой главный вывод стал довольно простым: архитектурное решение стоит оценивать не только по тому, какую проблему оно решает сегодня, но и по тому, какое новое состояние, зависимость или границу оно добавляет в систему. И, пожалуй, именно понимание этих границ оказалось важнее выбора конкретного фреймворка.
Полезные ссылки
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.