Ruby Backend Engineering: как строят системы, которым доверяют

- -
- 100%
- +

Ruby Backend Engineering: как строят системы, которым доверяют
2025
ПРЕДИСЛОВИЕ
Я пишу эту книгу из конкретного места — из опыта разработчика, который прошёл путь от первых HTML-страниц в двенадцать лет в Бийске Алтайского края до руководителя группы разработки на федеральных цифровых системах России. Это не биографическая деталь ради биографии. Это важно потому, что каждая глава книги основана на реальных задачах, ошибках и решениях, не на синтетических примерах из учебников.
Я работал над системами, которым люди доверяли кое-что серьёзное. Ошибка в статусе торговой процедуры здесь — это не баг, который исправят в следующем спринте. Это юридические последствия. Центральная цифровая платформа: миллионы заявок, вся страна, данные о жизненно важной инфраструктуре. Система для арбитражных процедур, где каждый документ имеет юридическую силу.
Именно работа с такими системами сформировала инженерное мышление, которое эта книга пытается передать. Не «как написать код, который работает», а «как строить системы, которым доверяют».
О чём эта книга
Книга посвящена backend-инженерии на Ruby on Rails, не как языку синтаксиса или фреймворку для CRUD, а как инженерной платформе для построения сложных, нагруженных, юридически и финансово значимых систем. Двадцать три главы охватывают путь от устройства языка Ruby до open source и публичной экспертизы.
Книга состоит из четырёх частей. Первая часть закладывает фундамент: Ruby как язык инженерного мышления, Rails за пределами CRUD, доменная модель в монолите, API-first архитектура, тестирование как архитектурный инструмент. Вторая часть посвящена данным и производительности: PostgreSQL в production, ActiveRecord и его подводные камни, очереди и асинхронность, Redis, Elasticsearch, управление файлами. Третья часть — о production: Docker, AWS-инфраструктура, наблюдаемость, инциденты, безопасность. Четвёртая часть — о зрелой инженерии: модернизация монолита, архитектурная декомпозиция, критические бизнес-операции, инженерные стандарты, наставничество, open source.
Каждую часть можно читать независимо, но книга выстроена как путь: от языка через данные к production и к инженерной зрелости.
Откуда взялась эта книга

Интерес к программированию появился рано. В двенадцать лет я начал делать первые сайты. В четырнадцать вместе с другом мы подняли игровые серверы на домашних компьютерах, создали сайт и продавали услуги хостинга. После девятого класса я поступил в Бийский государственный колледж на специальность «Информационные системы» и занял второе место на региональном чемпионате WorldSkills Russia в компетенции «Веб-дизайн».
В девятнадцать лет я устроился на первую работу как Ruby on Rails разработчик. Первые проекты: CRM-система для инвестиционно-финансовой компании, конструктор чат-ботов для бизнеса, блокчейн-приложения для хранения данных. Четыре года реального кода, реальных пользователей, реальных ошибок.
В 2020 году я перешёл в компанию, которая управляет системой электронных торгов и закупок. За шесть лет я вырос от разработчика до руководителя группы, каждый год получал премии по итогам оценки, получил благодарность от генерального директора. За это время я участвовал в модернизации платформы, переводил монолит с архитектуры Rails back + Rails front на Rails API + Nuxt, оптимизировал критические сервисы торгов, разработал систему управления жизненным циклом файлов — решение, которое снизило инфраструктурные расходы без ухудшения доступности для пользователей.
Параллельно я строил систему обучения: подготовил корпоративный курс по Ruby, проводил менторство, помог нескольким разработчикам вырасти до уровня middle+. Из этого опыта и родилась эта книга.
Для кого написана эта книга
Книга написана для разработчиков, которые уже знают Ruby on Rails и хотят двигаться дальше: понять, как строятся системы, а не просто как писать код. Для тех, кто хочет перейти от «работает у меня» к «надёжно работает в production». Для тех, кто думает о переходе в senior-позицию и хочет понять, чем отличается системное мышление от навыка написания кода.
Книга также будет полезна тем, кто уже работает на уровне senior и хочет структурировать свой опыт. Я обнаружил, что многое из того, что я делал интуитивно годами, стало гораздо яснее, когда я попробовал это сформулировать.
Это не учебник по синтаксису Ruby. Предполагается, что читатель умеет писать на Ruby и Rails. Это книга о том, как думать об инженерных задачах.
Как читать эту книгу
Если вы изучаете тему последовательно — читайте главы по порядку. Первая часть закладывает понятийный фундамент, без которого некоторые главы второй и третьей части будут менее понятны.
Если вы решаете конкретную задачу прямо сейчас — идите к нужной главе напрямую. Каждая глава самодостаточна в том смысле, что её можно читать отдельно и получить из неё практическую ценность.
Примеры кода в книге — реальные или основанные на реальных решениях. Я намеренно не упрощал их до «академических» примеров. Production-код сложнее учебного — и именно это делает его полезным.
Часть I.
Ruby и Rails как основа зрелого backend
ГЛАВА 1. RUBY КАК ЯЗЫК ИНЖЕНЕРНОГО МЫШЛЕНИЯ
Когда я впервые открыл редактор и написал первую строку на Ruby, мне было девятнадцать лет. За плечами был колледж, региональный чемпионат WorldSkills, пара самодельных сайтов и горячее желание понять, как устроены системы, которым люди доверяют деньги, данные и юридически значимые решения. Тогда я не думал о философии языка. Я просто хотел, чтобы код работал.
Но со временем понимаешь: выбор языка программирования — это не только вопрос синтаксиса или скорости выполнения. Это вопрос того, как ты думаешь о задаче. Ruby формирует способ рассуждать об объектах, поведении, ответственности и границах. И именно поэтому я начинаю книгу не с фреймворка, не с базы данных и не с архитектурных паттернов, а с самого языка.
Эта глава — попытка объяснить, почему Ruby располагает к определённому инженерному мышлению. Не потому что он лучший язык (такой постановки вопроса я принципиально избегаю), а потому что его устройство подталкивает разработчика к конкретным решениям. Читая её, вы, возможно, узнаете подходы, которые уже используете интуитивно. Или откроете для себя инструменты, которые давно лежали рядом, но казались лишними.
1.1. Почему Ruby — это не только синтаксис, а способ моделировать поведение
Среди разработчиков есть расхожее мнение: Ruby выбирают за красивый синтаксис, а потом страдают от производительности. Это мнение неточно по обеим частям. Синтаксис Ruby действительно выразительный, но это следствие, а не цель. Цель — позволить программисту описывать поведение системы так, как он описывает его в голове.
Мац (Юкихиро Мацумото, создатель Ruby) в своих интервью неоднократно говорил о том, что проектировал язык для людей, а не для машин. Эта фраза кажется маркетинговой, пока не начнёшь работать с языками, спроектированными в обратную сторону. Когда вы пишете на C или Java, вы часто думаете о том, как объяснить компилятору, что вы имеете в виду. В Ruby большую часть времени вы просто описываете, что должно происходить.
Возьмём простой пример. Представьте, что вам нужно найти все активные заявки пользователя, созданные за последние тридцать дней. На большинстве языков вы напишете запрос, пройдётесь по результату циклом и применимте условие. В Ruby запись выглядит иначе:
Application.where(user: user).active.created_after(30.days.ago)
Это не магия и не синтаксический сахар ради красоты. Это отражение того, как работает цепочка областей видимости (scopes) в ActiveRecord, построенная на принципах композиции объектов. Каждый вызов возвращает объект того же типа с дополненными условиями. Конечный запрос строится лениво, только когда данные действительно нужны.
Этот паттерн формирует привычку: описывать поведение через интерфейс объекта, а не через набор инструкций. Это не просто удобство. Когда кодовая база растёт, умение отделить «что» от «как» становится принципиально важным для поддерживаемости системы.
На проекте цифровой платформы для тендерных процедур, эта привычка спасала нас регулярно. Система обрабатывает тысячи торговых процедур, каждая из которых имеет сложный жизненный цикл: создание, подача заявок, проведение торгов, подведение итогов, подписание договора. Вся эта логика строилась на объектах с чётко определённым поведением, и это позволяло добавлять новые типы процедур, не трогая уже работающий код.
Ruby позволяет строить объекты так, что каждый из них несёт ответственность за свою область. Это звучит банально — принцип единственной ответственности описан в каждой второй книге по проектированию. Но именно в Ruby этот принцип встроен в идиомы языка. Блоки, лямбды, методы как объекты первого класса — всё это инструменты для создания систем, где поведение можно передавать, комбинировать и переопределять без перестройки архитектуры.
Посмотрите на схему ниже. Она наглядно показывает разницу между описанием задачи через инструкции и через объектный интерфейс.

Ruby — это язык, в котором идиоматичный код описывает намерение, а не механику. Это не означает, что он скрывает от вас устройство системы. Это означает, что язык не заставляет вас описывать механику, когда вам нужно описать намерение. Для большого проекта разница накапливается в сотнях строк кода, которые либо читаются как объяснение задачи, либо требуют дешифровки.
Переход от «написать код, который работает» к «написать код, который описывает поведение системы» — это сдвиг, который происходит не сразу. Я сам прошёл через него, работая над первыми проектами: CRM-системой для инвестиционно-финансовой компании, конструктором чат-ботов, проектами для хранения данных на базе блокчейн. Сейчас, когда я обучаю стажёров и провожу менторство, именно это различие пытаюсь донести в первую очередь.
1.2. Объектная модель Ruby: классы, модули, примеси и границы ответственности
В Ruby всё является объектом. Это утверждение слышат на первом занятии по языку, кивают и идут дальше. Но стоит разобраться, что именно это означает на практике, потому что последствия для архитектуры системы весьма существенны.
Число 42 в Ruby — это объект класса Integer. Строка «hello» — объект класса String. Класс MyService — тоже объект, только класса Class. Это означает, что класс можно передать в метод как аргумент, сохранить в переменную, вернуть из другого метода. Это не академическая деталь: паттерны, которые в других языках требуют фабрик, шаблонов или отражения, в Ruby реализуются напрямую.
Что из этого следует для реальной системы? Разные типы заявок обрабатываются по-разному: физические лица, юридические лица, заявки на догазификацию. Каждый тип имеет свою логику проверки данных, свой набор документов, свой процесс согласования. Один из подходов — большой блок условий по типу. Другой — передавать класс-обработчик как параметр и вызывать его методы полиморфно. Ruby не просто поддерживает второй подход — он делает его естественным.
Теперь о модулях. Модуль в Ruby — это пространство имён и инструмент для подмешивания (mixin) поведения в класс. Разница между модулем и классом проста: от модуля нельзя создать экземпляр, и у него нет единственного предка. Зато его можно включить в любой класс с помощью include, и все методы модуля станут методами экземпляра класса.
Это мощный инструмент для совместного использования поведения без наследования. Классическая проблема объектно-ориентированного проектирования: что делать, когда поведение нужно разделить между классами, которые не имеют смысла в одной иерархии? В Ruby для этого существуют примеси.
Пример из реальной работы: на платформе системы электронных торгов и закупок у нас было несколько сущностей (процедуры, заявки, договоры), каждая из которых должна была поддерживать аудит изменений. Логика аудита одинакова: при каждом изменении фиксировать, что изменилось, кто изменил, когда. Вместо того чтобы дублировать этот код или выстраивать искусственную иерархию, мы создали модуль Auditable и включили его в нужные классы. Чистое, тестируемое решение, которое не усложняет иерархию объектов.

Объектная модель Ruby подталкивает к тому, чтобы каждая единица кода (класс, модуль, метод) делала что-то одно и делала это хорошо. Но это подталкивание, а не принуждение. Можно написать класс на тысячу строк, который делает всё что угодно — Ruby не запретит. Именно поэтому важно понимать, какие конструкции языка поддерживают правильное разделение, и использовать их осознанно.
Несколько ориентиров, которые я выработал за годы работы с большими приложениями. Если метод делает больше одного логического шага — это сигнал: либо метод слишком большой, либо он знает о деталях, которые не должен знать. Если класс включает больше двух-трёх модулей и каждый из них большой — это почти наверняка признак того, что класс взял на себя слишком много. Если вы не можете в одном предложении описать ответственность класса — класс либо делает несколько вещей, либо его назначение размыто.
Понимание объектной модели — это фундамент. Но фундамент имеет смысл только тогда, когда на нём что-то строится. Следующий вопрос: как отличить Ruby-код, написанный с пониманием этой модели, от кода «на любом языке с Ruby-синтаксисом»?
1.3. Идиоматичный Ruby против кода на любом языке с Ruby-синтаксисом
Когда команда нанимает нового разработчика с опытом в других языках, и тот начинает писать на Ruby, первое время код выглядит странно. Технически правильно, синтаксис верный, тесты проходят — но что-то не так. Обычно это называют «неидиоматичным кодом».
Что такое идиоматичность? Это использование возможностей языка так, как их задумал автор и как принято в сообществе. Неидиоматичный код — это Ruby-синтаксис поверх мышления в другой парадигме. Такой код работает, но его сложнее читать тому, кто знает Ruby, и он обычно упускает возможности языка, которые сделали бы код короче и выразительнее.
Конкретный пример. Разработчик с опытом Java пишет метод для получения имени пользователя или значения по умолчанию:
1 вариант
2 вариант
def display_name(user)
if user != nil
user.name
else
"Гость"
end
end
def display_name(user)
user&.name || "Гость"
end
Разница не только в длине. Первый вариант говорит: «если пользователь не пустой, вернуть имя, иначе вернуть гость». Второй говорит: «имя пользователя, если пользователь есть, иначе гость». Это чуть ближе к тому, как мы думаем о задаче, а не к тому, как процессор её выполняет.
На большом проекте это накапливается. Кодовая база платформы для проведения электронных закупок насчитывает сотни тысяч строк. Если каждый разработчик пишет в своём стиле, новый человек (или тот же разработчик через полгода) тратит значительно больше времени на понимание чужого кода. Единый идиоматичный стиль — это форма документации.
Важная оговорка: идиоматичность не самоцель и не религия. Иногда явный цикл читается лучше, особенно если тело цикла сложное или выполняет несколько действий. Всегда нужно спрашивать: какая запись лучше всего передаёт намерение в конкретном контексте? Если идиоматичная запись требует от читателя знания пяти специфичных методов Ruby, возможно, более явный вариант предпочтительнее.
Идиоматичный код — это не конечная цель, а условие, при котором следующая тема имеет смысл. Метапрограммирование строится именно на глубоком понимании объектной модели и идиом языка. Без этого фундамента оно становится источником магии, а не силы.
1.4. Метапрограммирование: где оно усиливает систему, а где разрушает читаемость
Метапрограммирование — одна из тех тем, которые вызывают диаметрально противоположные реакции у разработчиков. Одни смотрят на возможности define_method, method_missing, class_eval как на то, что делает Ruby уникальным. Другие считают это источником нечитаемого кода и инцидентов в production. Правы и те и другие. Вопрос в том, где граница.
Что такое метапрограммирование в Ruby? Это способность программы изучать и изменять свою структуру во время выполнения. Классы можно открывать и добавлять методы. Методы можно определять динамически по имени. Можно перехватить вызов несуществующего метода и обработать его. Это не экзотика: именно на этих возможностях построены некоторые из самых часто используемых частей Rails.
Рассмотрим ActiveRecord. Когда вы определяете класс User < ApplicationRecord, вы получаете методы find, where, create, update, destroy — и при этом не пишете ни строки их реализации. ActiveRecord читает схему базы данных и динамически создаёт методы-атрибуты для каждого столбца. Это метапрограммирование на службе у разработчика: удобный интерфейс без дублирования кода для каждой таблицы.
Теперь о том, где метапрограммирование разрушает читаемость. Главная опасность — когда оно скрывает связи, которые должны быть видны. С одной стороны, это убирает дублирование. С другой — попробуйте найти метод render_details в кодовой базе с помощью обычного поиска. Вы не найдёте его. Редактор не сможет предложить автодополнение. Если в логике метода есть ошибка, трассировка стека покажет его имя, но перейти к «определению» стандартными средствами будет невозможно.

Правило, которое я выработал: метапрограммирование оправдано, когда оно устраняет значительное дублирование и когда его присутствие явно обозначено в архитектуре системы. Хороший признак правильного применения: разработчик, не знакомый с этим местом кода, может понять, что происходит, за разумное время. Плохой признак: «магия», которую нужно долго объяснять новому человеку в команде.
Отдельно стоит сказать о monkey-patching — открытии существующих классов и добавлении в них методов. В Rails это используется повсеместно: 5.days.ago, 'hello'.titleize, [1,2,3].sum работают именно потому, что Rails открывает классы Integer, String, Array и добавляет в них методы. Это удобно и делает Ruby-код выразительным. Но в прикладном коде monkey-patching опасен: вы можете случайно затронуть поведение, которое используется в других частях системы. Безопасная альтернатива — Refinements, механизм, позволяющий расширять классы в ограниченной области видимости.
Понимание метапрограммирования — это понимание того, где возможности языка заканчиваются и начинается ответственность разработчика. Следующий вопрос, который из этого вырастает: как написать код, который можно поддерживать не только сегодня, но и через три года?
1.5. Как писать Ruby-код, который можно поддерживать годами
За шесть лет работы на одном проекте я видел код, написанный до меня, и код, написанный мной три года назад. Это разный опыт. Код, написанный до тебя, вызывает вопросы: почему именно так? Что здесь происходит? Можно ли это изменить? Свой старый код вызывает другие чувства: я помню контекст, но не всегда помню детали. И если код хорошо написан, контекст восстанавливается по нему самому.
Поддерживаемый код — это не красивый код и не короткий код. Это код, который объясняет свои намерения и делает очевидными свои ограничения. Несколько конкретных принципов, выработанных на практике.
Первый принцип: имена важнее комментариев. Если метод называется process_data, вы ничего не знаете о том, что он делает. Если он называется validate_and_persist_application, вы знаете многое. Хорошее имя устраняет необходимость в комментарии. Плохое имя требует комментария, который со временем устаревает, пока код меняется, а комментарий остаётся нетронутым.
Второй принцип: явное лучше неявного. Когда метод принимает хэш опций без явного описания, что именно в этом хэше ожидается, — это источник проблем. Когда метод принимает именованные параметры (keyword arguments) с явными именами — читатель видит контракт сразу.
Третий принцип: мутации должны быть очевидны. Ruby позволяет методам изменять объект на месте (bang-методы: sort! вместо sort, upcase! вместо upcase). Когда метод изменяет состояние объекта, это должно быть отражено либо в имени (persist!, complete!), либо в документации. Скрытые побочные эффекты — один из самых сложных источников ошибок в больших системах.
Четвёртый принцип, который я особенно ценю: fail fast. Если метод получает некорректные данные, он должен сразу сигнализировать об этом, а не пытаться продолжить работу с неверными предположениями. В системах, где ошибка означает некорректный статус торгов или неверно принятую заявку на газификацию, «продолжить работу как-нибудь» означает создать данные, которые потом невозможно восстановить без ручного вмешательства.
На практике это выражается в явных проверках в начале метода:
def submit_application(user, params) raise ArgumentError, 'Пользователь не передан' unless user raise InvalidStateError unless user.verified? raise DuplicateApplicationError if user.has_pending_application? # основная логика только здесь end
Пятый принцип, который часто недооценивают: думайте о контексте чтения, а не о контексте написания. Когда вы пишете код, у вас в голове полный контекст задачи. Когда кто-то другой (или вы сами через год) читает этот код, этого контекста нет. Хорошо написанный код должен воссоздать этот контекст через имена, структуру и, где необходимо, через комментарии. Комментарий уместен не для объяснения того, что делает код, а для объяснения того, почему именно так.
1.6. Баланс выразительности, простоты и предсказуемости
Ruby соблазняет выразительностью. Язык богатый: у него много способов сделать одно и то же, много конструкций, которые «работают как по-английски», много синтаксических удобств. Это приятно при написании кода, но может создать проблемы в большой команде или долгоживущем проекте.
Покажу на примере. В Ruby есть несколько способов написать условие: if / unless, тернарный оператор, постфиксный if, конструкция case/when. Все они правильны. Но если в кодовой базе они используются вперемешку без ясного принципа, код становится труднее читать — не потому что конструкции сложны, а потому что читатель должен каждый раз переключаться между стилями.
Простота в Ruby — это не упрощение задачи. Это устранение лишних уровней абстракции там, где задача не требует их. Рефакторинг к простоте часто выглядит как удаление кода, а не добавление.
Предсказуемость — другой аспект. Код предсказуем, когда его поведение понятно из интерфейса без необходимости читать реализацию. Метод save_order должен сохранять заказ. Если он кроме этого отправляет уведомление, обновляет статистику и записывает в лог — это непредсказуемое поведение, которое рано или поздно приведёт к ошибке.

Именно из этого баланса — выразительность там, где она помогает, простота там, где можно, предсказуемость всегда — вырастает то, что называют хорошим Ruby-кодом. Это не правило из книги. Это культура, которая формируется в команде через ревью, обсуждения и общие примеры.
Ruby — язык, который предоставляет разработчику высокую степень свободы. Эта свобода работает на вас, если вы используете её осознанно, и против вас, если злоупотребляете ею. Баланс выразительности, простоты и предсказуемости — это навык, который приходит с опытом и рефлексией.
Разобрав объектную модель, идиомы и принципы поддерживаемого кода, можно сформулировать наблюдение, которое редко звучит явно: Ruby — это язык, в котором инженер постоянно принимает решения о выразительности. В отличие от языков с более жёсткими ограничениями, Ruby не принуждает к единственному способу решить задачу. Это делает его мощным инструментом для опытных разработчиков и потенциально трудным для тех, кто ещё не выработал внутренние критерии выбора.
Именно поэтому изучение Ruby стоит начинать не с синтаксиса, а с вопроса: что я хочу выразить этим кодом? Когда разработчик может ответить на этот вопрос применительно к каждому методу и каждому классу — он пишет код, который понятен и через три года. Когда не может — пишет код, который работает сегодня и становится проблемой завтра.



