В мире разработки программного обеспечения понятие модульности играет ключевую роль в обеспечении управляемости и расширяемости проектов, особенно тех, которые постоянно растут и развиваются. Компания Gusto, занимая лидирующую позицию в сфере решений для payroll, HR и бенефитов, активно внедряет современные методики модульности, нацеленные на повышение качества кода и ускорение процесса разработки. В 2024 году инженерная команда Gusto представила обновленное видение и инструментарий, позволяющие эффективно управлять большим кодовым массивом — речь идет о новой ступени эволюции модульности их программных продуктов. Рассмотрим более подробно, с чего все начиналось и к чему команда пришла сегодня. История развития модульности в Gusto началась вскоре после выхода open source проекта Packwerk от Shopify в 2020 году.
Уже тогда команда Gusto увидела потенциал в подходе, который предлагает разбиение большого приложения на масштабируемые и изолированные пакеты. Через несколько недель после релиза Packwerk на компании было создано около двухсот таких пакетов, что помогло структурировать основной код, визуально отделить домены и сформировать архитектуру, отвечающую требованиям масштабирования. Однако первые пакеты были далеки от идеала — в них имелось множество пересечений и нарушений границ, что усложняло дальнейшее развитие. Следующая фаза развития включала расширение числа пакетов более чем до четырёхсот. Несмотря на непростую внутреннюю организацию, такой прирост пакетов позволял командам по-разному взглянуть на структуру продукта и начать отчетливо понимать, как именно должны взаимодействовать различные зоны системы.
Однако возникла проблема: рост количества пакетов превратился в новую головоломку — сложность понимания и ориентации в архитектуре возрастала, мешая эффективной работе с кодом. В 2024 году Gusto вступило в фазу переосмысления этой модели. Инженеры задали себе фундаментальный вопрос: «Что такое приложение внутри нашего кода?». Выяснилось, что вместо 400 пакетов можно выделить около 20 приложений, что намного проще для восприятия и сопоставимо с продуктами и функциями, которые реально используют клиенты. Это значительное уменьшение числа единиц структуры позволило бы более эффективно отобразить бизнес-логику, при этом сохранив внутренние границы и порядок.
Реорганизация кода была невозможна за счет простого слияния пакетов — разработчики нуждались в сохранении возможности модульно разрабатывать внутри своих доменов. Поэтому команда создала несколько важных инструментов и концепций, среди которых появились понятия слоев, продуктов-сервисов и вложенных пакетов. Слои архитектуры дополняют концепцию выделенных приложений-продуктов, которые теперь в Gusto называют «product services». Выявилась закономерность в организации кода: есть несколько сотен пакетов, которые принадлежат одному из 20 продуктов-сервисов, а также отдельные группы пакетов, не привязанные к одному сервису. К этим отдельным группам относятся корневое приложение Rails (application harness), инструменты для разработки и настройки окружения, базовые классы Rails и универсальные утилиты.
Важным наблюдением стало то, что взаимодействие между слоями идет по определенной направленности — сверху вниз. Верхний слой не должен зависеть от нижних, что помогает избежать циклических зависимостей и повышает предсказуемость архитектуры. Чтобы лучше визуализировать и зафиксировать эту иерархию, команда переместила пакеты в соответствующие папки, разделив код на уровни: tooling, product_services, rails_shims и utilities. Этот подход повысил наглядность и позволил с помощью пакетных правил packwerk-extensions реализовать защиту слоев, предотвращая нарушение зависимостей между ними. Примерно 80% кода сосредоточилось в слое product_services, что стало естественным ядром для построения сложных бизнес-логик.
Важной инновацией стала настройка вложенности пакетов. Внутри folder product_services теперь находятся отдельные приложения (примерно 20 штук), которые в свою очередь содержат пакеты, организующие внутреннюю логику каждого сервиса. Благодаря вложенности появилась возможность поддерживать баланс между глобальной размерностью и детализацией компонентов, без необходимости жертвовать тонкой модульностью. Однако именно этот подход породил новый вопрос: как определить и разграничить API? Ведь если product services считаются приложениями, их внешние интерфейсы должны иметь четкие ограничения и специфику. Но в то же время пакеты внутри этих приложений, как правило, функционируют больше как библиотеки, менее ограниченные в коммуникациях и зависимостях.
Здесь команда Gusto столкнулась с опытом, аналогичным ранее описанному в анализе Shopify. Ожидания, связанные с идеальной изоляцией, не оправдались, и потребовалось выработать более практическое понимание разграничения внешних и внутренних интерфейсов. Примером проблемы стала рекомендация «избегать использования ActiveRecord на границе пакетов». Для полноценного приложения такое требование имеет смысл — обмен данными идет через сети или строго определённые сервисные интерфейсы, поэтому объекты ORM должны быть преобразованы в транспонируемые структуры. Но в рамках одного приложения, где пакеты тесно связаны, это ограничение приводит к усложнению кода, снижению производительности и дополнительным затратам на разработку слоя посредника.
Для графовых запросов GraphQL, где пакеты в продукт-сервисах строят дерево данных, невозможность напрямую использовать внутренние модели ActiveRecord приводит к ненужным накладным расходам — вызываются лишние преобразования, увеличивается сложность кэширования и возникают проблемы с загрузкой данных (например, распространённая проблема n+1 запросов). Поэтому в Gusto решили разделить API продукта и внутренние API пакетов. Для внешнего общедоступного интерфейса каждого product service создается специальный пакет «_api». Все остальные пакеты являются внутренними и не должны быть частью внешнего контакта. Чтобы контролировать это, внедряются механизмы visibility и приватности папок с помощью расширений packwerk.
Это обеспечивает баланс: внутри product services команды могут спокойно работать с нужными зависимостями и свойствами, а между product services внедряется строгая политика приватности и правил зависимостей, предотвращающая нарушения. Одновременно для удобства работы команд введены паттерны игнорирования, позволяющие минимизировать шум от сообщений о нарушениях. В итоге архитектура стал эффективным компромиссом между детальностью, масштабируемостью и бизнес-ориентированностью — product services соответствуют ключевым продуктам и функциям, что облегчает управление, коммуникацию и развитие. Внедренные инструменты обеспечивают видимость границ, надежную защиту API и поддержку внутренней модульности одновременно. С технической точки зрения инструментальная база Gusto также совершила серьезный шаг вперед.
Компания отказалась от начального инструмента packwerk в пользу собственного ускоренного аналога pks, написанного на Rust. Pks обеспечивает более быструю работу, более точное статическое сканирование всех Ruby файлов для поиска констант и зависимостей. Этот инструмент также поддерживает современные расширения, такие как сложные шаблоны игнорирования и настройку видимости. Таким образом, разработки Gusto в области модульности демонстрируют глубокое понимание баланса между архитектурной дисциплиной и практическими реалиями разработки крупных продуктов. Выработаны структурные подходы, которые помогают синхронизировать технические архитектурные цели с бизнес-эффективностью и удобством работы команд.
Будущее видится в дальнейшем развитии и автоматизации правил модульности, увеличении осознанности команд и их способности создавать качественные API, которые станут фундаментом для новых функций и возможностей в эко-системе Gusto. В конечном итоге такой подход служит не только техническим целям, но и обеспечивает высокую ценность для клиентов, предлагая гибкие, надежные и легко поддерживаемые решения. Комьюнити Ruby и Rails уже проявляют интерес к опыту Gusto, обсуждая сложности и решения внедрения модульности в крупных проектах. Это способствует обмену знаниями и развитию лучших практик по всему сообществу, что в конечном счете ведет к более качественным и эффективным программным продуктам в глобальном масштабе.