Микросервисы прагматика: как построить большую систему с помощью пачки монолитов

Полезная статья для девопсов и сисадминов

2024-03-09 13:42:21 - ERneSt⚡️os

Как быстро зафейлить новый проект Java? Просто взять и применить все, что ты услышал на последней Java конференции;) Как быстро сделать энтерпрайзный проект минимальной командой в короткие сроки? Верно — подобрать оптимальную архитектуру и правильные инструменты. Senior Developer из команды Jmix Дмитрий Черкасов рассказывает о компромиссном варианте между хайповыми (все еще) микросервисами и монолитами, который называется Self-Contained Systems. Кажется, он выпьет меньше крови и сохранит ваши нервы. Дальше — рассказ от первого лица.


Отмечу, что мы будем называть Self-Contained Systems «автономные системы» или кратко — SCS.

Как следует из названия, принцип подхода прост — разбиваем систему по доменам. В каждом домене находится независимый монолит (система). Стараемся максимально не допускать связность между подсистемами. Напоминает микросервисы? Но не делайте поспешных выводов!

Пройдя «Вьетнам» на одном энтерпрайз проекте с SCS-подобной архитектурой, я захотел сделать R&D по автономным системам. Хотелось бы поделиться опытом исследований и рассказать как готовить SCS. Какой спектр задач может покрыть новоявленная концепция и для каких проектов SCS станет идеальным решением?

Как я узнал о SCS

Как-то на одном из дейликов или планерок мы с коллегами подняли срач дискутировали на тему таргет аудитории Jmix. Можно ли написать огромный маркетплейс без колоссальных усилий на платформе? А так как я уже знал ответ на этот вопрос, пройдя медные трубы в кровавом энтерпрайзе, у меня возникла мысль написать проект, демонстрирующий, что можно сделать, используя монолиты в 24 году (куда большего, чем вы могли бы представить).

Очевидно, в бигтехе — Сбере, Озоне, ВТБ и других явова (см. ЯВОВА/ВОТВАСЯ) все архитекторы будут воротить нос, как только услышат слово «монолит». В общем, у кого-то это щелкает, а у кого-то это не щелкает.

В первую очередь, SCS, как и сама статья, будут интересны людям, занимающимся E-commerce приложениями. Насмотревшись на кровавый энтерпрайз, могу сказать, если у вас скудный ресурс, а система большая, то SCS может стать целебной таблеткой, решающей многие проблемы среднестатистического проекта.

Вторая часть аудитории — проекты, использующие современные фуллстак-фреймворки. Я, как разработчик фреймворка Jmix, а также любитель Ruby on Rails, могу заявить, что написание SCS будет для вас занимательным опытом. Ну или хотя бы прочтение данной статьи позабавит вас, как и мои цитаты Тинькова.

Когда не стоит смотреть в сторону SCS?

Первое, если у вас большая нагрузка (в основном это касается UI), сложный и нестандартный UI, который проще написать на современных фронтенд фреймворках с использованием SOA/микросервисов. В общем, не тривиальный UI — не для нас. Второе, если в проекте какие-то вычисления или риал-тайм работа с данными, проще будет сразу писать микросервисы.

Стоит отметить, что все перечисленные выше проблемы обычно не касаются энтерпрайза.

Детальное погружение

Индикатор того, что ваш проект будет хорошо ложиться на SCS — наличие большого количества доменов и бизнес-логики, связанной с заполнением всяких форм. Узнали? Это же наш (не)любимый энтерпрайз. Главное преимущество, которое получает проект при разделении на автономные системы — это «микросервисные монолиты». В таком подходе мы имеем повышенную устойчивость, деплой и другие плюшки, о которых я расскажу далее.

Если говорить душно более научно, то:

Self Contained Systems — это независимые, самодостаточные и слабосвязанные системы (подсистемы), каждый из которых представляет собой отдельную часть функциональности в рамках большого программного продукта.

Перейдем на простонародный язык. Автономные системы — это «сообщающиеся монолиты» с некоторым количеством «но», заимствованных из микросервисов. Главные концепции — минимальная связанность между системами и отказоустойчивые коммуникации.

Disclaimer. У самурая нет плана архитектуры — есть только путь задачи!

В правильных руках и монолит может стать крайне эффективной архитектурой (см. GitHub, GitLab, Shopify). Не относитесь к статье как к постулатам! Зачастую, выбор архитектуры может быть делом вкуса и скиллов команды.


Innoq - самый большой меинтейнер в комьюнити SCS.

Хотелось бы вкратце рассказать о главной акуле SCS — Innoq. С ресурсов и сайта Innoq взято большинство контента, представленного в статье. Если перейти по ссылке, можно почитать историю об успешном успехе компании, построившие свою E-Commerce систему на базе Self-Contained Systems. Скриншот ниже, взятый с сайта Innoq, примерно знакомит нас с архитектурой SCS.

Как мы можем наблюдать, каждая из подсистем Innoq — это набор монолитов, разбитых на домены. Подмечу, есть общая система, которая собирает все разрозненные элементы UI вместе. Сейчас опустим этот момент, но чуть позже я объясню, зачем нужен этот элемент системы.

Чтобы лучше прочувствовать разницу между SOA и микросервисами, я добавил картинку ниже, демонстрирующую разницу в этих подходах.

Философия Self-Contained Systems!


Сообщество энтузиастов сформулировали некоторое количество принципов, следуя которым можно написать хорошую систему, построенную на SCS. На сайте SCS можно детально почитать про данные принципы.

Основные принципы:

Ниже находится пересказ всех поинтов, описанных комьюнити и Innoq.

От себя добавил некоторые недосказанные поинты, а где-то добавил комментарии. Здесь не будет никаких искажений оригинала, лишь объяснения. Мое мнение о работе SCS в реальном мире я выскажу чуть позже.

Все эти принципы по отдельности носят лишь рекомендательный характер, не стоит слишком сильно подгонять свою систему под все вышесказанные поинты. Но чем меньше вы отступаете от постулатов, тем меньше проблем вы получите в перспективе.


Ключевые аспекты философии:

Ниже комментарии от меня, которые поясняют некоторую недосказанность философии Self-Contained Systems.


Плюсы, минусы

Теперь обсудим что мы получаем, выбрав SCS. На какие грабли можно наступить в таком подходе?


Плюсы:МинусыМинусы в реалиях энтерпрайза

Однако все эти недостатки могут быть менее релевантными для B2B и E-Commerce систем, где количество пользователей вторично по сравнению с множеством бизнес-процессов и объемом бизнес-логики. Круто? ДА это же круто.


Back to reality: Демо-пример и выбор объектной модели

Той еще проблемой для меня стал выбор темы демо. CRM? Пэт-клиника? Онлайн магазин? Public Cloud? Ох, вы бы знали сколько я думал над тем, что же выбрать... В итоге, взвесив все плюсы и минусы, я решил, что мой выбор — доставка еды. Эта тема не только предоставляет широкий спектр бизнес-процессов, но и близка к повседневной реальности аудитории. Ну кто хоть раз не пользовался доставкой еды? И понять легко и бизнес-процесс прозрачный и большой;)


План работ

Раз с темой разобрались, пора переходить к планированию. В выбранной области стоит выделить:


Далее опустим некоторое количество правил, которые нам будут сильно усложнять работу. Обычно такие правила являются жизненно важными для бизнеса, но с хай-левел точки зрения являются мелочами. Например, мы не будем обрабатывать случай, когда курьер поскользнулся на льду и расшибся:)


Доставка еды

Пожалуй, иконка проекта выбрана. А может мне стоило попросить DALL-e сгенерировать ее вместо меня... Кстати, на иконке спойлеры =)

Пора выбрать основные домены внутри доставки еды, без которых понятие доставки не может работать.

Ресторан (Restaurant System):

В этом домене фокусируется вся информация о ресторанах. Здесь осуществляется регистрация и создание новых ресторанов. Администраторы ресторанов имеют возможность настраивать меню, добавлять и редактировать блюда, а также принимать заказы на готовку еды. Этот домен является ключевым для организации бизнес-процесса внутри ресторанов.


Доставка (Delivery):

Домен доставки фокусируется на управлении курьерами. Здесь происходит регистрация курьеров, назначение им роли доставщика, а также менеджмент заказов, связанных с доставкой. В этом домене осуществляется эффективное управление логистикой и выполнением заказов.


Заказы (Order System):

Центральным доменом в системе доставки еды является система заказов. Здесь пользователи начинают бизнес-процесс, создавая заказы. Интерфейс для создания заказов, наполнение корзины и все запросы, связанные с бизнес-процессом доставки еды, исходят именно из этого домена. Здесь происходит взаимодействие с другими подсистемами и интеграции для эффективного выполнения заказов.


Важное отступление перед демо:

В реальной системе должны существовать дополнительные домены, без которых доставка не имеет смысла. Эти домены включают систему платежей, уведомлений, построения маршрутов доставки и триггеров геолокации. Они представляют собой важные аспекты реальной системы, но в рамках демонстрации SCS мы упростим структуру и сфокусируемся на интеграциях между основными подсистемами.

Перед проведением демо Self-Contained Systems я решил упростить некоторые аспекты бизнес-процесса доставки еды. Мы не будем рассматривать сложные сценарии, такие как оплата и отмена заказов, чтобы минимизировать сложность написания кода и ускорить прототипирование проекта. Роли будут минимальными для удобства работы, и мы учтем другие аспекты, которые упростят и ускорят процесс написания доставки еды. Наша основная цель — продемонстрировать эффективные интеграции между различными подсистемами.

Основные задачи для демо
  1. Flow-диаграмма и архитектура: Начнем с создания Flow-диаграммы и определения архитектуры проекта. Это поможет нам лучше понять взаимодействие между системами.
  2. Выбор технологического cтека: На основе Flow-диаграммы мы определимся с технологическим стеком. Какие инструменты и языки программирования будем использовать. Хотя спойлеры вы выше видели.
  3. Пробежимся по коду: Ну как статья может быть и без кода... Такого быть не может. Если кто-то утверждает обратное — да на кол его! Если без шуток, то в этом пункте я укажу главные куски кода отвечающие за интеграции между системами, да и то в виде скрывающихся деталей. Если хотите полный проект — я оставил ссылку на Github репозиторий.
  4. Переход к демонстрации: Быстро пробежимся по результату, которого мы добились.
Взглянем на систему свысока

У нас есть три ключевые подсистемы и три типа пользователей.

Подсистемы:

Flow-диаграмма и BPMN

Ниже представлена Flow-диаграмма того, что будет происходить в системе. Да. Она немножечко страшная;) А вы видели простые Flow-диаграммы?

Стоит сделать отступление, что будет еще один шаг, который мы опустим. Это шаг самой физической доставки курьером от точки А до точки Б. Вместо этого мы просто подождем 10 секунд и сразу поменяем статус заказа на «доставлен».

Для более наглядного представления мы решили вместо Flow-диаграммы использовать более высокоуровневую BPMN-диаграмму.

Это более интуитивное представление процесса. В общих чертах, мы создали простой сценарий:

  1. Пользователь заходит в систему заказов. Система заказов загружает список ресторанов из системы ресторанов (какая-то тавтология).
  2. Пользователь выбирает ресторан. Система загружает из системы ресторанов список еды и меню для пользователя. Пользователь составляет корзину из предложенного и сохраняет заказ.
  3. Система заказов сохраняет копии заказа пользователя и запускает бизнес-процесс.
  4. Система заказов делает HTTP-запрос в систему ресторанов с запросом на готовку еды. Процесс останавливается и ожидает, пока ресторан подтвердит.
  5. Админ ресторана заходит в систему (ресторанов), берет заказ на приготовление еды. Система ресторанов делает обратный HTTP-запрос в систему доставки еды. Система заказов продолжает бизнес-процесс.
  6. Система заказов делает HTTP-запрос в систему курьеров и публикует запрос на доставку.
  7. Курьер заходит в систему доставки, берет на себя текущий заказ. Система курьеров делает обратный HTTP-запрос в систему заказов, продолжается бизнес-процесс.
  8. Начинается доставка. Здесь мы имитируем доставку и другие элементы путем ожидания 10 секунд. Этот этап пустой. Затем бизнес-процесс приостанавливается, ожидая завершения доставки.
  9. Курьер доставил еду. Заходит в систему и подтверждает окончание доставки. Система курьеров делает обратный HTTP-запрос в систему заказов. Бизнес-процесс продолжается.
  10. Финальный шаг. Заказ переводится в статус «доставлен», и бизнес-процесс завершается.
UI и принципы разделения по доменам

Учитывая полную автономию и независимость наших трех систем, мы избегаем использования фреймов или ссылок, так как нет необходимости в установлении жестких связей между ними. Каждая из трех систем эффективно функционирует независимо от других.

Из-за высокой степени автономии между системами нам не требуется создавать отдельную систему, которая бы служила корневой для всех подсистем и предоставляла бы контент в виде iframe-ов для каждой из них. Вместо этого мы предпочитаем поддерживать независимость каждой системы, не нарушая ее автономии.

Такой подход дает нам гибкость и свободу в разработке каждой подсистемы, а также упрощает управление и поддержку проекта в целом, поскольку каждая система может эффективно разрабатываться и внедряться независимо от остальных.

Для самых любознательных я приведу краткий пример системы с общим layout-ом в конце статьи.

Выбор технологий

Мысли сбежавшего из дурки о выборе технологий

При выборе технологий для реализации архитектурного паттерна Self-Contained Systems в энтерпрайз-проекте моем демо учтем не только функциональные требования, но и особенности объектной области. А у нас есть некоторый функционал, которые можно покрыть реализациями из коробки.


В данном случае, я остановился на языке программирования Java со Spring Boot-ом. Но вы думаете что я буду использовать просто Бут, будучи разработчиком другого фреймворка?

Конечно же воспользуемся Jmix! С его помощью мы получаем не только современный UI, но и встроенный движок Bpmn, что важно для обеспечения потребностей бизнес-процессов.

Дополнительно, Flowable используется в качестве Bpmn 2.0 движка, предоставляя необходимую асинхронность и отказоустойчивость, которые являются критическими компонентами для успешной работы в корпоративной среде. В контексте управления пользователями между системами было принято решение использовать Keycloak. Этот выбор обусловлен не только простотой интеграции, но и созданием единой точки управления пользователями, что обеспечивает удобство и единообразие.

Итак, технологический стек нашего проекта включает в себя:Архитектура коммуникаций

Настоятельно рекомендую тщательно ознакомится с тезисами, изложенными ниже, тк они фактически решают какие границы ответственности мы используем.

В данном примере мы сознательно упрощаем сценарий, демонстрируя, что для построения устойчивой системы не всегда необходимо использовать асинхронные и/или реактивные методы коммуникации.

  1. При посещении пользователем страницы заказа мы предоставляем ему все объекты передачи данных (DTO), полученные из ресторанов. Это гарантирует, что до начала процесса и формирования заказа мы не вмешиваемся в консистентность данных.
  1. При создании заказа пользователем мы сохраняем копии выбранных им блюд, обеспечивая, таким образом, надежность в случае изменения или удаления элементов из меню ресторана.
  2. Все взаимодействия остаются внутри процесса. С учетом того, что мы пишем процесс с использованием Bpmn 2.0 и движка Flowable, запросы будут отправляться асинхронно.
  3. Мы реализуем паттерн Request-Wait-ResponseAfter через Flowable:

При получении ответа от другой системы мы возобновляем пользовательскую задачу, и процесс продолжает свой ход. Этот подход решает множество проблем, связанных как с устойчивостью, так и с минимизацией межсистемных коммуникаций.


Реализация в двух словах

Изначально статья должна была быть детальной, но если затянуть с объяснением всего, то кажется ее бы можно было изложить только в 12 часовой видос на youtube или небольшую книгу. Потому буду давать фрагменты кода и комментарии. Начнем сначала.


Создание базового функционала Restaurant System

Нужен CRUD для ресторанов, еды и меню внутри ресторана. Так как мы пишем на Jmix — у нас есть возможность написать полноценный сайт, а не использовать Rest API и какой-нибудь Postman.

Сущности первой необходимости:

Пропущу код, связанный с созданием UI, лишь скажу, что мы создали:


Прикрутим Rest для того, чтобы можно было из системы Заказов доставать список ресторанов и их содержимого:

RestaurantController


@Secured(FullAccessRole.CODE)
@RestController
@RequestMapping(value = "api/v1")
public class RestaurantController {

    private final RestaurantRepository restaurantRepository;
    private final DataManager dataManager;
    private final AttachmentService attachmentService;
    private final FetchPlans fetchPlans;
    private final RestaurantMenuRepository restaurantMenuRepository;

    @GetMapping("/restaurants")
    public List<RestaurantDTO> listRestaurants() {
        return StreamSupport.stream(restaurantRepository.findAll().spliterator(), false)
                .map(restaurant -> {
                    var dto = new RestaurantDTO();
                    dto.setId(restaurant.getId());
                    dto.setName(restaurant.getName());
                    dto.setDescription(restaurant.getDescription());
                    dto.setIcon(attachmentService.getAttachmentAsByteArray(restaurant));
                    return dto;
                })
                .toList();
    }

    @GetMapping("/restaurants/{id}")
    public RestaurantDTO getRestaurant(@PathVariable Long id) {
        Restaurant restaurant = restaurantRepository.getById(id);
        var dto = new RestaurantDTO();
        dto.setId(restaurant.getId());
        dto.setName(restaurant.getName());
        dto.setDescription(restaurant.getDescription());
        dto.setIcon(attachmentService.getAttachmentAsByteArray(restaurant));
        return dto;
    }

    @GetMapping("/restaurants/{restaurantId}/menus")
    public List<RestaurantMenuDTO> listRestaurants(@PathVariable Long restaurantId) {
        return restaurantMenuRepository.findRestaurantMenuByRestaurantId(restaurantId)
                .map(menu -> {
                    var menuDTO = dataManager.create(RestaurantMenuDTO.class);
                    menuDTO.setId(menu.getId());
                    menuDTO.setName(menu.getName());
                    menuDTO.setItems(convertItemsToDTO(menu.getItems()));
                    return menuDTO;
                })
                .toList();
    }
}

Итак, как только мы написали все необходимые эндпоинты, можем перейти к настройке ресторанов. Создадим пару ресторанов и еду в них, чтобы было с чем работать в системе заказов.

Предлагаю создать StarBucks, а то в последнее время его не хватает. Хотя нет, чего действительно не хватает — это Сбер Пиццы. Реализуем же фантазии, кто знает, возможно это станет правдой...

Заполним рестораны. Остановимся только на сберпицце)

  1. Детали


Еда

Меню, они же табы для клиентов

Думаю со cбера хватит. Переходим к OrderService.

На этом этапе нам необходим лишь функционал создания корзины, все остальное будет реализовано в следующих шагах с бизнес-процессом.

RestaurantClient


@Component
  public class RestaurantClient extends AbstractClient {

    public List<RestaurantDTO> listRestaurants(String subjectToken) {
        String url = MessageFormat.format("{0}/api/v1/restaurants", caclRestaurantUrl());
        return getApi(url, HttpMethod.GET, new ParameterizedTypeReference<List<RestaurantDTO>>() {}, null, subjectToken);
    }

    public RestaurantDTO getRestaurantById(Long restaurantId, String subjectToken) {
        String url = MessageFormat.format("{0}/api/v1/restaurants/{1,number,#}", caclRestaurantUrl(), restaurantId);
        return getApi(url, HttpMethod.GET, new ParameterizedTypeReference<RestaurantDTO>() {}, null, subjectToken);
    }

    public List<RestaurantMenuDTO> listRestaurantMenus(Long restaurantId, String subjectToken) {
        String url = MessageFormat.format("{0}/api/v1/restaurants/{1,number,#}/menus", caclRestaurantUrl(), restaurantId);
        return getApi(url, HttpMethod.GET, new ParameterizedTypeReference<List<RestaurantMenuDTO>>() {}, null, subjectToken);
    }
}

Опустим лишние детали, нас тут интересует только то, что все «стартовые» запросы процесса по доставке еды могут быть синхронные, т.к. используются только на стороне юзера для UI.

Перейдем к проверке работы корзины, сделаем заказ
  1. Выберем ресторан. КОНЕЧНО ЖЕ СБЕР ПИЦЦА


Удостоверимся, что все меню и еда пришла верная

Соберем инвестиционный портфель. Ой, то есть соберем корзину еды, конечно же.

Отлично, корзина готова и можно приступать к самому интересному — бизнес процессу, который мы обозначили пару топиков назад.


Бизнес процесс всему голова!

Так как у нас есть Flowable и есть реальный бизнес процесс, который можно описать, то построим всю доставку еды на BPMN:

Все те же шаги, что и в том красивом бизнес процессе. Осталось дело за малым - привязать все ServiceTask (те что с колесиком) к Java коду.

Больше деталей: как UserTask решит все мои проблемы

Многие задумались, а как я буду решать проблему, когда возникает некоторое ожидание, когда мы ждем готовку еды или когда ищем курьера?

И тут самая главная идея BPMN нам помогает!

UserTask — это объект нотации bpmn, когда бизнес процесс перестает исполняться и встревает на UserTask в ожидании, когда таску продолжат. То есть, мы пойдем исполнять по очереди java код для каждой сервис таски, пока не наткнемся на первую UserTask-у. Чтобы «разморозить» исполнение бизнес-процесса, нам понадобиться специально найти эту таски и сказать «а-ну продолжайся!».

Короткий экскурс в bpmn окончен. Погнали к делу:

Как только наш заказчик подтвердил свою корзину, мы начнем бизнес-процесс и присвоим ему статус НАШЕЙ КОРЗИНЫ (заказа). Получается, зная, в каком мы инстансе бизнес-процесса, мы знаем и номер заказа.

Остается только запустить процесс из экрана, когда заказчик нажимает кнопку подтверждения (я тут нарочно избегаю слово клиент во избежание путаницы со словом SomeHttpClient).

    public ProcessInstance startOrderProcess(String orderId) {
        AppUser appUser = (AppUser) currentAuthentication.getUser();

        Map<String, Object> processVariables = Map.of(
                PROCESS_USER_KEY, appUser.getUsername()
        );
        return runtimeService.startProcessInstanceByKey(ORDER_PROCESS_SCHEMA_ID, orderId, processVariables);
    }

И сразу пройдем к первому шагу. Присваиваем ему статус нового и сохраняем в транзакционный контекст.

    @Override
    protected void doTransactionalStep(DelegateExecution execution, OrderEntity order, SaveContext saveContext) {
        order.setStatus(DraftOrderStatus.NEW_ORDER);
        saveContext.saving(order);
        doSomeWork();
    }
Асинхронные ServiceTask и bpmn паттерн Request-Wait-ResponseAfter

Далее все наши ServiceTask будут исполняться в отдельных потоках, будут помечены асинхронным флагом и принадлежать к разным транзакциям, тем самым мы даем гарантии, что наш код будет всегда максимально изолирован и устойчив к ошибкам. Да, да, бизнес процесс можно еще и откатывать)

Не буду освещать все шаги, так как они однотипны, лишь рассмотрим один общий паттерн, по которому проходит весь бизнес процесс:


  1. Мы делаем Http запрос в асинхронной ServiceTask в другую систему с запросов «сготовьте еду в таком то ресторане»

RequestRestaurantCookStep


     @Service
     public class RequestRestaurantCookStep extends AbstractTransactionalStep {
         private final Logger log = LoggerFactory.getLogger(RequestRestaurantCookStep.class);
         private final RestaurantClient restaurantClient;
         private final OrderService orderService;
         
         @Override
         protected void doTransactionalStep(DelegateExecution execution, OrderEntity order, SaveContext saveContext) {
             String username = getVariable(execution, PROCESS_USER_KEY);
             String subjectToken = exchangeOidcTokenForUser(username);
             String result = systemAuthenticator.withUser(username,
                     () -> restaurantClient.publishRestaurantCookRequest(order.getRestaurantId(), orderService.convert(order), subjectToken));
             log.info("Result from restaurant system for cook request: " + result);
             order.setStatus(DraftOrderStatus.WAIT_FOR_RESTAURANT);
             saveContext.saving(order);
             doSomeWork();
         }
     }

RestaurantClient#publishRestaurantCookRequest


 public String publishRestaurantCookRequest(Long restaurantId, OrderDTO orderDTO, String subjectToken) {
     String url = MessageFormat.format("{0}/api/v1/restaurants/{1,number,#}/cook", restaurantUrl, restaurantId);
     return getApi(url, HttpMethod.POST, new ParameterizedTypeReference<String>() {}, orderDTO, subjectToken);
 }
  1. При обработке запроса на готовку в системе ресторанов сохраняется запрос для дальнейшего отображения админам.

RestaurantController#getRestaurantCookRequest


 @PostMapping("/restaurants/{restaurantId}/cook")
 public String getRestaurantCookRequest(@PathVariable Long restaurantId, @RequestBody OrderDTO orderDTO) {
     cookOrderService.submitNewCookOrderFromDTO(orderDTO);
     // we will not bring case that restaurant will not cook, placeholder response
     return "Accepted";
 }

CookOrderService#submitNewCookOrderFromDTO


 public void submitNewCookOrderFromDTO(OrderDTO orderDTO) {

     var cookOrderRequest = create(CookOrderRequest.class);
     cookOrderRequest.setOrderId(orderDTO.getOriginOrderId());
     cookOrderRequest.setIsDone(false);
     cookOrderRequest.setRestaurant(restaurantRepository.getById(orderDTO.getRestaurantId()));
     cookOrderRequest.setCookingItems(createCookingListFromDTO(cookOrderRequest, orderDTO));

     save(cookOrderRequest);
 }
  1. Админ ресторана заходит в список задач на готовку, берется за нее, тем самым отправляет обратный Http запрос обратно в систему заказов.

OrderProcessController#continueOrderRestaurantStep


@PostMapping("/orders/{orderId}/restaurantstep/{restaurantId}")
     public void continueOrderRestaurantStep(@PathVariable String orderId, @PathVariable String restaurantId) {
         orderProcessManager.continueProcessByRestaurantStep(orderId, restaurantId);
     }
  1. Когда приходит Http запрос от системы ресторанов (ответ на запрос о готовке) - мы продолжаем наш бизнес-процесс далее

OrderProcessManager#continueProcessByRestaurantStep


  @PostMapping("/orders/{orderId}/restaurantstep/{restaurantId}")
     public void continueOrderRestaurantStep(@PathVariable String orderId, @PathVariable String restaurantId) {
         orderProcessManager.continueProcessByRestaurantStep(orderId, restaurantId);
     }
  1. СУУУУПЕР ГУД. Request-Wait-ResponseAfter реализован в бизнес-процессе.
  2. Далее делаем то же самое для шагов с поиском курьера и подтверждением окончания доставки от курьера.

Усложнять статью не буду, потому опущу код, связанный с точно такими же шагами с курьером и сервис-тасками, которые просто меняют статус заказа.


Подведем промежуточные итоги для текущего демо проекта:

  1. Мы построили проект доставки еды в стиле Self-Contained Systems;
  2. Проект получился весьма устойчивым к ошибкам;
  3. Связность между подсистемами минимальна;
  4. Большинство поинтов философии SCS были поддержаны (в том числе асинхронные и отказоустойчивые коммуникации);
  5. По времени это заняло сущие копейки.
А теперь все бежим переписывать наши монолиты на Self-Contained Systems


Альтернативный вариант Self-Contained Systems через iframe

Но все же! Мы не использовали iframe и проект получился не очень связным в UI и все связи находятся только на уровне коммуникаций. Предлагаю вкратце рассмотреть пример, как можно написать SCS при помощи iframe.

Данный вариант построения самодостаточных систем является более каноничным, т.к. на сайте в первых поинтах сразу упоминают iframe. Хоть я и не считаю, что такой подход имеет большой потенциал (достаточно трудно найти подходящий бизнес-сценарий), мы обязаны его рассмотреть.

Перейдем на Amazon.com, возьмем как пример веб-магазин, у которого существует множество поддоменов(подсистем):

  1. В меню борде мы видим список разных ссылок на подсистемы, которые могут быть реализованы отдельными «монолитами». Их мы и выделим в качестве подсистем. Но для подсистем надо иметь общий layout (common space).


  1. В то время как для контента подсистем надо выделить большое пространство, где мы будем отображать UI выделенной подсистемы, которую пользователь выберет
  2. Следовательно, декомпозиция на подсистемы с общей UI системой будет выглядеть примерно так:


Что происходит когда пользователь заходит на такой сайт?


  1. Загружается страница с общим layout и каким-то placeholder контентом по центру, где у нас пространство iframe. Либо мы сразу подгружаем дефолтную подсистему для пользователя в iframe space.
  2. В меню есть ссылки, при нажатии на которые мы подменяем iframe-ы, каждый из которых ведет в свою подсистему.
  3. При подгрузке iframe так же прокидываются cookie, поэтому пользователь, залогинившийся в одну общую систему, считай залогинился во все.

Пример взят из видео-ряда англоязычного контент-мейкера с ютуба.


Подведем итоги

Надеюсь, я развлек вас сколько-то интересным контентом. А в ваших руках появился еще один инструмент решений проблем в проектировании ПО.

More Posts