📎 Главная проблема в микросервисах — 26 июля 2026 г. в 10:28:00.329
📎 Главная проблема в микросервисах Сервис стартует чистым. Через полгода его трогать никто не хочет. Багов нет, тесты проходят, но любое изменение задевает пять файлов, а новому инженеру три дня объясняют, что где лежит. Проблема не в алгоритме и не в кэше, а в структуре. Команды копируют раскладку папок, не понимая контракт, который эта раскладка должна навязывать. Папки вместо архитектуры Спросите десять команд про структуру сервиса, и почти все опишут что‑то такое. /cmd /internal /handlers /services /repository /models /pkg Выглядит разумно и проходит ревью. Беда в том, что папки создают иллюзию границ, но не держат их. Ничто не мешает хендлеру импортировать репозиторий напрямую. Ничто не мешает модели обрасти бизнес‑логикой. Границы косметические, и код деградирует предсказуемо. Логика переползает туда, где удобнее, обычно в хендлеры, потому что там контекст. Типы репозитория протекают в API, потому что заводить отдельный DTO лень. Пакет models превращается в общий мешок структур, от которого зависят все, и поменять в нём что‑либо без каскада уже нельзя. Получается слоёная на вид архитектура, ведущая себя как монолит с накладными расходами на HTTP. В чём контракт слоёв Контракт про направление зависимостей. Они текут внутрь. Внешний слой (HTTP, gRPC, CLI) знает про слой приложения. Слой приложения знает про домен. Домен не знает ни про что. На практике это значит вот что. Хендлер принимает запрос, зовёт метод сервиса и сериализует ответ. Никакого SQL и никаких бизнес‑правил в нём нет. Сервис кодирует, что приложение делает. Он оркестрирует, валидирует, делегирует репозиторию и ничего не знает про HTTP. Репозиторий говорит с базой и возвращает доменные типы или ошибки, но не pgx.Rows. Доменные типы несут инварианты. Структура Member знает, что делает участника валидным, но не знает, как сериализовать себя в JSON. Когда контракт держится, слои тестируются независимо, реализацию репозитория можно подменить, не трогая хендлер, а gRPC добавить без дублирования логики. Анти‑паттерн god service Чаще всего контракт ломает god service. Сервис‑структура, которая всасывает всё: type MemberService struct { db *pgxpool.Pool } func (s *MemberService) CreateMember(ctx context.Context, req CreateMemberRequest) (*Member, error) { // валидация, хеш, вставка строки, welcome‑письмо, событие в Kafka } Через полгода у MemberService сорок методов, пул базы, почтовый клиент, продюсер Kafka, клиент S3 и конфиг. Любой тест требует поднять все зависимости, даже когда проверяешь одну ветку валидации. Это уже не слой, а ящик для хлама. И дробить его на MemberCreationService и MemberUpdateService бесполезно, получите ящики поменьше. Лечит инверсия зависимостей Интерфейсы в Go удовлетворяются неявно, и это недоиспользуют. Вместо *pgxpool.Pool опишите то, что сервису реально нужно: type MemberRepository interface { CreateMember(ctx context.Context, params CreateMemberParams) (*Member, error) GetMemberByID(ctx context.Context, id uuid.UUID) (*Member, error) } type EventPublisher interface { Publish(ctx context.Context, event DomainEvent) error } type MemberService struct { repo MemberRepository publisher EventPublisher } Теперь сервис ничего не знает про PostgreSQL и Kafka. Он зависит от поведения, а не от реализации. Чтобы протестировать метод, достаточно мока под интерфейс, без базы, брокера и сети. Про pgx знает реализация репозитория, про franz-go знает реализация publisher, а сервис не знает ни про то, ни про другое. В этом и есть контракт.

