Тактические паттерны DDD: Value Object
Начнем с примера простой сущности:
По мне, так абсолютно невыразительная сущность. Структура Клиент вообще никак себя не характеризует. Просто какой-то набор полей с примитивными типами.
Смотрим глубже: представим, что эти базовые типы, относящиеся к клиенту, будут везде: в аргументах юзкейсов, репозиториев и т.п. Печальная картина.
А теперь взглянем на сущность здорового человека:
А также некоторые выдуманные интерфейсы:
Какая красота (подумали вы). Вот это и есть Value Objects (VO сокращенно):
🔸Сущность характеризуется своими value-объектами. VO отражают бизнес-концепции.
🔸VO не имеет идентификатора.
🔸Является неизменяемым (immutable). Нельзя поменять поля структуры Address, придется пересоздать новый адрес. Следовательно код не должен содержать никаких сеттеров для value-объектов.
🔸Инкапсулирует в себя бизнес-логику и валидацию.
🔸Существует в контексте сущности.
🔸Если необходимо сравнить два VO, нужно сравнивать значения их полей.
🔸Для создания сложного VO - можно использовать фабричные методы с валидацией.
🔸Как VO должен храниться в БД?
- Поля VO это колонка в таблице сущности
- Поле jsonb (маршаллинг)
- Редко можно вынести в отдельную таблицу, но тогда не забываем, что для VO не должно быть своего репозитория, он загружается вместе с сущностью
#DDD
Начнем с примера простой сущности:
type Client struct {
id uuid
phone string
email string
address string
}По мне, так абсолютно невыразительная сущность. Структура Клиент вообще никак себя не характеризует. Просто какой-то набор полей с примитивными типами.
Смотрим глубже: представим, что эти базовые типы, относящиеся к клиенту, будут везде: в аргументах юзкейсов, репозиториев и т.п. Печальная картина.
А теперь взглянем на сущность здорового человека:
type Client struct {
id ClientID
phone Phone
email Email
address Address
}
type ClientID uuid
type Phone string
func (s Phone) String() string {
return string(s)
}
func (s Phone) Validate() error {
// логика по валидации
return nil
}
type Email string
func (s Email) String() string {
return string(s)
}
type Address struct {
street string
city string
country string
}
func NewAddress(street, city, country string) (Address, error) {
if street == "" || city == "" || country == "" {
return Address{}, errors.New("invalid address")
}
return Address{street, city, country}, nil
}
func (a Address) Street() string {
return a.street
}
func (a Address) IsEqual(addr Address) bool {
return a.street == addr.Street() &&
a.city == addr.City() &&
a.country == addr.Country()
}
А также некоторые выдуманные интерфейсы:
type ClientRepository interface {
Load(ctx context.Context, id ClientID) (*Client, error)
LoadByEmail(ctx context.Context, email Email) (*Client, error)
}Какая красота (подумали вы). Вот это и есть Value Objects (VO сокращенно):
🔸Сущность характеризуется своими value-объектами. VO отражают бизнес-концепции.
🔸VO не имеет идентификатора.
🔸Является неизменяемым (immutable). Нельзя поменять поля структуры Address, придется пересоздать новый адрес. Следовательно код не должен содержать никаких сеттеров для value-объектов.
🔸Инкапсулирует в себя бизнес-логику и валидацию.
🔸Существует в контексте сущности.
🔸Если необходимо сравнить два VO, нужно сравнивать значения их полей.
🔸Для создания сложного VO - можно использовать фабричные методы с валидацией.
🔸Как VO должен храниться в БД?
- Поля VO это колонка в таблице сущности
- Поле jsonb (маршаллинг)
- Редко можно вынести в отдельную таблицу, но тогда не забываем, что для VO не должно быть своего репозитория, он загружается вместе с сущностью
#DDD
👏2🫡2
Тактические паттерны DDD: Aggregate
Ну вот мы и подошли к самому интересному. Кто-то считает что агрегаты это самое важное в технической части DDD. Кто-то их использует очень редко. Для меня это очень сложное понятие, давайте разбираться.
Если сущность это объект, то агрегат - это концепция (понятие) предметной области. Например, заказ или финансовая транзакция.
🔸Представляет собой кластер объектов: объединяет связанные сущности и value-объекты в одну логическую единицу. Другими словами, агрегат управляет состоянием связанных объектов.
🔸Order - это корень агрегата. Это значит, что всё взаимодействие должно идти только через него. Нельзя просто так взять и создать или модифицировать OrderItem. Агрегат отвечает за целостность данных.
🔸Во многих процессах есть непреклонные бизнес-правила, как например нельзя добавить вещь в заказ у которой цена или количество равны нулю. Такие бизнес-правила называются инвариантами. Эти инварианты должны отражаться в модели (агрегатах, сущностях, VO)
🔸Агрегат сохраняется в БД как единое целое, в транзакции. Кстати, загружаются тоже целиком.
🔸Изменение состояния возможно только через вызов методов. Как и в сущностях.
🔸Агрегат может быть связан с другим агрегатом через ID.
🔸Внимание: сущность или VO не может существовать отдельно от агрегата:
1. Каждый раз добавляя сущность или VO, думаем к какому агрегату он принадлежат.
2. Если об этом не подумать, то мы не сможем гарантировать валидное состояние системы.
3. Давайте примем факт, что возможны исключения. Например, для простых, самодостаточных сущностей. Сущность сама может быть агрегатом.
🔸Внимание 2: как понять надо ли объединять два агрегата? Если они никак не связаны инвариантами - это два разных агрегата.
🔸Может ли быть несколько агрегатов внутри одного контекста? Может, но их не должно быть много. Если в рамках одного контекста нужно изменить несколько агрегатов, это делается через разные транзакции или сагу.
Агрегаты в одном контексте должны быть высоко связаны! Должны решать какую-то общую задачу.
🔸Как взаимодействуют агрегаты в одном контексте? Связь через ID, я об этом говорю выше. Через доменные события или взаимодействие происходит в сервисах.
🔸Божество :) Дада, не надо делать божественные агрегаты, лучше несколько.
Накидывайте в комментах кто что думает, где я не прав? может что-то упустил? нам нужна истина!
Повторить: https://github.com/MaksimDzhangirov/practicalDDD/blob/master/docs/aggregate.md
#DDD
Ну вот мы и подошли к самому интересному. Кто-то считает что агрегаты это самое важное в технической части DDD. Кто-то их использует очень редко. Для меня это очень сложное понятие, давайте разбираться.
Если сущность это объект, то агрегат - это концепция (понятие) предметной области. Например, заказ или финансовая транзакция.
// Агрегат
type Order struct {
id OrderID
status Status
// Связь с другим агрегатом
customerID CustomerID
items []OrderItem
}
// Сущность
type OrderItem struct {
productID ProductID
quantity Quantity
price Money
}
// Value Object
type Money int64
func NewOrder(id OrderID, customerID CustomerID) *Order {
return &Order{
id: id,
status: New,
customerID: customerID,
items: make([]OrderItem, 0),
}
}
func (o *Order) AddItem(productID ProductID, quantity Quantity, price Money) error {
if quantity <= 0 {
return errors.New("quantity must be positive")
}
if price <= 0 {
return errors.New("price must be positive")
}
o.items = append(o.items, OrderItem{
productID: productID,
quantity: quantity,
price: price,
})
return nil
}
func (o *Order) Total() Money {
var total int64
for _, item := range o.items {
total += int64(item.price) * int64(item.quantity)
}
return Money(total)
}
func (o *Order) Cancel() error {
if o.status == Paid {
return errors.New("cannot cancel paid order")
}
o.status = Cancelled
return nil
}
🔸Представляет собой кластер объектов: объединяет связанные сущности и value-объекты в одну логическую единицу. Другими словами, агрегат управляет состоянием связанных объектов.
🔸Order - это корень агрегата. Это значит, что всё взаимодействие должно идти только через него. Нельзя просто так взять и создать или модифицировать OrderItem. Агрегат отвечает за целостность данных.
🔸Во многих процессах есть непреклонные бизнес-правила, как например нельзя добавить вещь в заказ у которой цена или количество равны нулю. Такие бизнес-правила называются инвариантами. Эти инварианты должны отражаться в модели (агрегатах, сущностях, VO)
🔸Агрегат сохраняется в БД как единое целое, в транзакции. Кстати, загружаются тоже целиком.
🔸Изменение состояния возможно только через вызов методов. Как и в сущностях.
🔸Агрегат может быть связан с другим агрегатом через ID.
🔸Внимание: сущность или VO не может существовать отдельно от агрегата:
1. Каждый раз добавляя сущность или VO, думаем к какому агрегату он принадлежат.
2. Если об этом не подумать, то мы не сможем гарантировать валидное состояние системы.
3. Давайте примем факт, что возможны исключения. Например, для простых, самодостаточных сущностей. Сущность сама может быть агрегатом.
🔸Внимание 2: как понять надо ли объединять два агрегата? Если они никак не связаны инвариантами - это два разных агрегата.
🔸Может ли быть несколько агрегатов внутри одного контекста? Может, но их не должно быть много. Если в рамках одного контекста нужно изменить несколько агрегатов, это делается через разные транзакции или сагу.
Агрегаты в одном контексте должны быть высоко связаны! Должны решать какую-то общую задачу.
🔸Как взаимодействуют агрегаты в одном контексте? Связь через ID, я об этом говорю выше. Через доменные события или взаимодействие происходит в сервисах.
🔸Божество :) Дада, не надо делать божественные агрегаты, лучше несколько.
Накидывайте в комментах кто что думает, где я не прав? может что-то упустил? нам нужна истина!
Повторить: https://github.com/MaksimDzhangirov/practicalDDD/blob/master/docs/aggregate.md
#DDD
GitHub
practicalDDD/docs/aggregate.md at master · MaksimDzhangirov/practicalDDD
Practical DDD in Golang. Contribute to MaksimDzhangirov/practicalDDD development by creating an account on GitHub.
👍3🫡3👏2
Тактические паттерны DDD: Domain Events
Это сообщения, отражающие значимые изменения в домене (OrderPlaced, UserRegistered, MoneyTransfered).
Я уже писал, что доменные события это один из способов интеграции ограниченных контекстов. Ну и как следствие - слабая связность компонентов. Кроме этого, взаимодействие через события упрощает масштабирование системы.
Характеристики: именование в прошедшем времени, immutable, может содержать контекст (что произошло, когда, связанные данные, например ID).
И теперь самое важное - событие генерируется агрегатом:
🔸Вызов метода агрегата может возвращать событие.
🔸Или агрегат накапливает события в себе и есть метод, который возвращает все накопленные события.
Хорошо, мы получили события агрегата, что с ними делаем дальше?
🔸Используем паттерн медиатор https://github.com/mehdihadeli/Go-MediatR .
🔸Отправляем эти ивенты в брокер с использованием паттерна Outbox.
Некоторые правила:
🔸События - часть домена. Должны лежать рядом с агрегатами.
🔸Поддерживают семантическое версионирование. Если изменилась структура события, создаем OrderPlacedV2.
🔸Идемпотентность. Обработчики событий должны корректно обрабатывать повторные события.
🔸Событий не должно быть слишком много. Стараемся отражать только значимые изменения.
Ссылки:
https://habr.com/ru/companies/lamoda/articles/678932/ - Паттерн Outbox
https://github.com/MaksimDzhangirov/practicalDDD/blob/master/docs/domain-event.md - Событие предметной области
#DDD
Это сообщения, отражающие значимые изменения в домене (OrderPlaced, UserRegistered, MoneyTransfered).
Я уже писал, что доменные события это один из способов интеграции ограниченных контекстов. Ну и как следствие - слабая связность компонентов. Кроме этого, взаимодействие через события упрощает масштабирование системы.
// Event базовый интерфейс для всех событий
type Event interface {
EventName() string
OccurredAt() time.Time
}
// OrderPlaced событие оформления заказа
type OrderPlaced struct {
EventID uuid.UUID
OrderID uuid.UUID
CustomerID uuid.UUID
Items []OrderItemDTO
CreatedAt time.Time
}
func (e OrderPlaced) EventName() string {
return "OrderPlaced"
}
func (e OrderPlaced) OccurredAt() time.Time {
return e.CreatedAt
}
Характеристики: именование в прошедшем времени, immutable, может содержать контекст (что произошло, когда, связанные данные, например ID).
И теперь самое важное - событие генерируется агрегатом:
🔸Вызов метода агрегата может возвращать событие.
🔸Или агрегат накапливает события в себе и есть метод, который возвращает все накопленные события.
// Агрегат из прошлого поста
type Order struct {
id OrderID
status Status
customerID CustomerID
items []OrderItem
// собираем ивенты
events []Event
}
// Place оформление заказа
func (o *Order) Place() error {
if o.status != Draft {
return errors.New("order cannot be placed")
}
o.status = Placed
// Генерация события
event := OrderPlaced{
EventID: uuid.New(),
OrderID: o.id,
CustomerID: o.customerID,
Items: o.convertItemsToDTO(),
CreatedAt: time.Now(),
}
o.events = append(o.events, event)
return nil
}
// PullEvents возвращает события и очищает список
func (o *Order) PullEvents() []Event {
events := o.events
o.events = nil
return events
}
Хорошо, мы получили события агрегата, что с ними делаем дальше?
🔸Используем паттерн медиатор https://github.com/mehdihadeli/Go-MediatR .
🔸Отправляем эти ивенты в брокер с использованием паттерна Outbox.
Некоторые правила:
🔸События - часть домена. Должны лежать рядом с агрегатами.
🔸Поддерживают семантическое версионирование. Если изменилась структура события, создаем OrderPlacedV2.
🔸Идемпотентность. Обработчики событий должны корректно обрабатывать повторные события.
🔸Событий не должно быть слишком много. Стараемся отражать только значимые изменения.
Ссылки:
https://habr.com/ru/companies/lamoda/articles/678932/ - Паттерн Outbox
https://github.com/MaksimDzhangirov/practicalDDD/blob/master/docs/domain-event.md - Событие предметной области
#DDD
GitHub
GitHub - mehdihadeli/Go-MediatR: 🚃 A library for handling mediator patterns and simplified CQRS patterns within an event-driven…
🚃 A library for handling mediator patterns and simplified CQRS patterns within an event-driven architecture, inspired by csharp MediatR library. - mehdihadeli/Go-MediatR
50👍4🫡3👏2
Тактические паттерны DDD: Domain Service
Это объект, который содержит логику, не принадлежащую ни одному агрегату или сущности.
Используется, когда:
🔸Для выполнении операции нужны внешние зависимости. Например, репозитории или клиенты к сторонним API.
🔸Операция затрагивает несколько агрегатов.
🔸Операция отвечает ТОЛЬКО за бизнес-логику.
Для себя я четко разделяю Domain Service и Application Service (Usecase). Подробнее о различиях я напишу в следующем посте. А пока что, для меня Domain Service должен инкапсулировать бизнес-логику, относящуюся к домену, а также важные бизнес-знания или инварианты. Например, определение дубликата какой-либо записи: если это важно в ограниченном контексте, то эти знания относятся к Domain Service.
Пример 2. Пусть у нас есть бизнес-правило: "Покупатель может оформить заказ только в том случае, если все товары в заказе есть в наличии на складе". Для проверки этого правила необходимо работать с двумя сущностями: Product и Order. Поскольку нет никаких инфраструктурных зависимостей, это чистая доменная логика. Следовательно, нам нужен OrderService.
Некоторые характеристики:
🔸Domain Service не должны быть божественными, лучше несколько небольших, относящихся к определенным задачам классов.
🔸Сервис не должен содержать логику, относящуюся к агрегату. Например, изменение статуса.
🔸Доменный сервис не должен обращаться к данным соседнего ограниченного контекста.
🔸Сервис может изменять только 1 агрегат.
🔸Доменный сервис используется на уровне Application Service.
Ссылки:
https://github.com/MaksimDzhangirov/practicalDDD/blob/master/docs/domain-service.md
#DDD
Это объект, который содержит логику, не принадлежащую ни одному агрегату или сущности.
Используется, когда:
🔸Для выполнении операции нужны внешние зависимости. Например, репозитории или клиенты к сторонним API.
🔸Операция затрагивает несколько агрегатов.
🔸Операция отвечает ТОЛЬКО за бизнес-логику.
package user
type EmailValidator struct {
userRepo UserRepository
}
func NewEmailValidator(repo UserRepository) *EmailValidator {
return &EmailValidator{userRepo: repo}
}
func (v *EmailValidator) IsEmailUnique(email string) (bool, error) {
existing, err := v.userRepo.FindByEmail(email)
if err != nil {
return false, err
}
return existing == nil, nil
}
Для себя я четко разделяю Domain Service и Application Service (Usecase). Подробнее о различиях я напишу в следующем посте. А пока что, для меня Domain Service должен инкапсулировать бизнес-логику, относящуюся к домену, а также важные бизнес-знания или инварианты. Например, определение дубликата какой-либо записи: если это важно в ограниченном контексте, то эти знания относятся к Domain Service.
Пример 2. Пусть у нас есть бизнес-правило: "Покупатель может оформить заказ только в том случае, если все товары в заказе есть в наличии на складе". Для проверки этого правила необходимо работать с двумя сущностями: Product и Order. Поскольку нет никаких инфраструктурных зависимостей, это чистая доменная логика. Следовательно, нам нужен OrderService.
Некоторые характеристики:
🔸Domain Service не должны быть божественными, лучше несколько небольших, относящихся к определенным задачам классов.
🔸Сервис не должен содержать логику, относящуюся к агрегату. Например, изменение статуса.
🔸Доменный сервис не должен обращаться к данным соседнего ограниченного контекста.
🔸Сервис может изменять только 1 агрегат.
🔸Доменный сервис используется на уровне Application Service.
Ссылки:
https://github.com/MaksimDzhangirov/practicalDDD/blob/master/docs/domain-service.md
#DDD
GitHub
practicalDDD/docs/domain-service.md at master · MaksimDzhangirov/practicalDDD
Practical DDD in Golang. Contribute to MaksimDzhangirov/practicalDDD development by creating an account on GitHub.
👍4👏3🫡2
Domain Service vs Application Service vs Use Case
Кратко, это всё сервисы, разница лишь в том, на каком уровне они работают.
Доменный сервис принадлежит уровню домена, инкапсулирует важные знания о предметной области и не должен иметь сложные зависимости на инфраструктуру.
Например, я работал в телекоммуникационной компании, и вот какие методы были в одном из доменных сервисах:
🔸IsAlreadyCommunicated - проверка на то, была ли уже коммуникация по номеру телефона. Важное знание? - Конечно, как определить, что мы уже звонили по этому номеру?
🔸CanCommunicateNow - часто есть ограничение на время звонка. Этот метод проверяет, можем ли мы сейчас позвонить?
🔸GetNextCommunicationTime - если мы позвонили, а номер был занят, нужно определить время перезвона.
Конечно, может возникнуть желание положить подобные методы на уровень юзкейса или апп. сервиса, но тут значимость метода определяет его уровень.
Часто, для простоты, Application Service приравнивают к Use Case, но иногда их различают по уровню абстракции.
Итак, Application Service находится на уровне приложения и координирует работу доменных объектов и инфраструктуры (репозиториев, внешних интеграций, отправки сообщений). Он может использовать Domain Service или соседние Application Service в монолите. Не должен содержать бизнес-логику, только координацию. На этом же уровне также находится и преобразование из DTO в сущности.
Use Case - это конкретный сценарий взаимодействия пользователя с системой. Очень похож на Application Service, но фокусируется на одной бизнес-операции, не зависит от инфраструктуры. Например, CreateTask, мне очень напоминает команды в CQRS.
Разобраться, что куда оказалось сложнее, чем я ожидал. Давайте подведем итог:
🔸Domain Service - содержит доменные знания.
🔸Use Case - это бизнес-логика для конкретного сценария. Не зависит от инфраструктуры, но может зависеть от Domain Service.
🔸Application Service - это оркестрация и взаимодействие с внешним миром. Может зависеть от одного или нескольких Use Case и Domain Service.
Еще упрощаем: берем для примера метод CreateOrder. Это может быть как отдельный Use Case, так и отдельный метод в Application Service. Как по мне, так если мы будем использовать все 3 уровня, архитектура сильно усложнится.
Пусть у нас есть PostgreSQL, Kafka и какие-то интеграции. Наличие кафки говорит нам сразу про Application Service. Вот и пускай остается только Application Service.
Или у нас Event Sourcing и CQRS: консьюмер прочитал ивент и вызвал команду. А команда реализована как отдельный Use Case.
Итого: пишем код не там где удобно, а там, где правильно, включаем голову.
Кратко, это всё сервисы, разница лишь в том, на каком уровне они работают.
Доменный сервис принадлежит уровню домена, инкапсулирует важные знания о предметной области и не должен иметь сложные зависимости на инфраструктуру.
Например, я работал в телекоммуникационной компании, и вот какие методы были в одном из доменных сервисах:
🔸IsAlreadyCommunicated - проверка на то, была ли уже коммуникация по номеру телефона. Важное знание? - Конечно, как определить, что мы уже звонили по этому номеру?
🔸CanCommunicateNow - часто есть ограничение на время звонка. Этот метод проверяет, можем ли мы сейчас позвонить?
🔸GetNextCommunicationTime - если мы позвонили, а номер был занят, нужно определить время перезвона.
Конечно, может возникнуть желание положить подобные методы на уровень юзкейса или апп. сервиса, но тут значимость метода определяет его уровень.
Часто, для простоты, Application Service приравнивают к Use Case, но иногда их различают по уровню абстракции.
Итак, Application Service находится на уровне приложения и координирует работу доменных объектов и инфраструктуры (репозиториев, внешних интеграций, отправки сообщений). Он может использовать Domain Service или соседние Application Service в монолите. Не должен содержать бизнес-логику, только координацию. На этом же уровне также находится и преобразование из DTO в сущности.
Use Case - это конкретный сценарий взаимодействия пользователя с системой. Очень похож на Application Service, но фокусируется на одной бизнес-операции, не зависит от инфраструктуры. Например, CreateTask, мне очень напоминает команды в CQRS.
Разобраться, что куда оказалось сложнее, чем я ожидал. Давайте подведем итог:
🔸Domain Service - содержит доменные знания.
🔸Use Case - это бизнес-логика для конкретного сценария. Не зависит от инфраструктуры, но может зависеть от Domain Service.
🔸Application Service - это оркестрация и взаимодействие с внешним миром. Может зависеть от одного или нескольких Use Case и Domain Service.
Еще упрощаем: берем для примера метод CreateOrder. Это может быть как отдельный Use Case, так и отдельный метод в Application Service. Как по мне, так если мы будем использовать все 3 уровня, архитектура сильно усложнится.
Пусть у нас есть PostgreSQL, Kafka и какие-то интеграции. Наличие кафки говорит нам сразу про Application Service. Вот и пускай остается только Application Service.
Или у нас Event Sourcing и CQRS: консьюмер прочитал ивент и вызвал команду. А команда реализована как отдельный Use Case.
Итого: пишем код не там где удобно, а там, где правильно, включаем голову.
👍4👏2🫡2
CAP-теорема
Это теорема о компромиссах в распределенных системах, которая гласит: Невозможно обеспечить все три свойства. Система может гарантировать только два из них.
Еще раз, эта теорема работает только для распределенных систем, если же распределения нет, системы могут обеспечивать все 3 свойства.
🔸C (consistency) - согласованность. Все узлы системы видят одни и те же данные в один момент времени.
Пример: После записи новых данных любой последующий запрос вернет актуальное значение.
🔸A (availability) - доступность. Система всегда отвечает на запросы, даже если часть узлов недоступна.
Пример: При сбое одного сервера клиенты продолжают получать ответы от оставшихся узлов.
🔸P (partition tolerance) - устойчивость к распределению. Система продолжает работать, несмотря на разрывы связи между узлами (network partition).
Пример: Два дата-центра теряют связь, но каждый из них обслуживает запросы локально.
Сейчас редко встретишь БД без репликации (single-node instance). Поэтому Partition Tolerance неизбежна. Остается сделать выбор между CP или AP.
🔸CP-системы: жертвуют доступностью ради согласованности.
Для систем где важна точность: финансовые транзакции, банкинг, бронирование.
- PostgreSQL (с синхронной репликацией). Если мастер-узел теряет связь с репликой, запись данных блокируется до восстановления соединения
- MongoDB (с настройками Write Concern = majority и Read Concern = majority). При сетевом разделе MongoDB теряет доступность до завершения выборов новой первичной реплики.
- Redis (с Redis Sentinel и минимальной настройкой для записи). В случае разделения сети Redis может отказаться принимать записи, если недостаточное количество реплик доступно.
- ZooKeeper. Если кластер разделяется на две части, меньшая часть перестает отвечать на запросы. Большая часть продолжает работать, сохраняя согласованность.
🔸AP-системы: жертвуют согласованностью ради доступности.
Для масштабируемости и отказоустойчивости: соцсети, мессенджер, IoT. В системах этого типа можно сгладить отсутствие согласованности через паттерн Сага.
- Memcached. Может использовать репликацию для повышения доступности, репликация обычно является асинхронной и не гарантирует строгую согласованность между репликами. Поддерживает eventual consistency за счет TTL на данные.
- Cassandra. В случае разделения сети может вернуть устаревшие или противоречивые данные. Однако, в конечном итоге, все данные будут согласованы (eventual consistency).
- Riak. Разрешает конфликты данных с помощью CRDT (Conflict-Free Replicated Data Types).
Это теорема о компромиссах в распределенных системах, которая гласит: Невозможно обеспечить все три свойства. Система может гарантировать только два из них.
Еще раз, эта теорема работает только для распределенных систем, если же распределения нет, системы могут обеспечивать все 3 свойства.
🔸C (consistency) - согласованность. Все узлы системы видят одни и те же данные в один момент времени.
Пример: После записи новых данных любой последующий запрос вернет актуальное значение.
🔸A (availability) - доступность. Система всегда отвечает на запросы, даже если часть узлов недоступна.
Пример: При сбое одного сервера клиенты продолжают получать ответы от оставшихся узлов.
🔸P (partition tolerance) - устойчивость к распределению. Система продолжает работать, несмотря на разрывы связи между узлами (network partition).
Пример: Два дата-центра теряют связь, но каждый из них обслуживает запросы локально.
Сейчас редко встретишь БД без репликации (single-node instance). Поэтому Partition Tolerance неизбежна. Остается сделать выбор между CP или AP.
🔸CP-системы: жертвуют доступностью ради согласованности.
Для систем где важна точность: финансовые транзакции, банкинг, бронирование.
- PostgreSQL (с синхронной репликацией). Если мастер-узел теряет связь с репликой, запись данных блокируется до восстановления соединения
- MongoDB (с настройками Write Concern = majority и Read Concern = majority). При сетевом разделе MongoDB теряет доступность до завершения выборов новой первичной реплики.
- Redis (с Redis Sentinel и минимальной настройкой для записи). В случае разделения сети Redis может отказаться принимать записи, если недостаточное количество реплик доступно.
- ZooKeeper. Если кластер разделяется на две части, меньшая часть перестает отвечать на запросы. Большая часть продолжает работать, сохраняя согласованность.
🔸AP-системы: жертвуют согласованностью ради доступности.
Для масштабируемости и отказоустойчивости: соцсети, мессенджер, IoT. В системах этого типа можно сгладить отсутствие согласованности через паттерн Сага.
- Memcached. Может использовать репликацию для повышения доступности, репликация обычно является асинхронной и не гарантирует строгую согласованность между репликами. Поддерживает eventual consistency за счет TTL на данные.
- Cassandra. В случае разделения сети может вернуть устаревшие или противоречивые данные. Однако, в конечном итоге, все данные будут согласованы (eventual consistency).
- Riak. Разрешает конфликты данных с помощью CRDT (Conflict-Free Replicated Data Types).
👍3🫡3
Антипаттерны доменной модели
🔸Анемичная модель (Anemic Domain Model)
Сущность не содержит никакой бизнес-логики (методов). По сути, сущность используется как DTO.
Это влечет за собой нарушение инкапсуляции, логика уезжает на прикладной уровень.
🔸ORM-анемия
Это частный случай анемичной модели, когда ORM (таблица) диктует структуру доменного объекта.
Кроме того, это нарушает SRP. Используем DTO для маппинга агрегата в репозитории.
Важно: сначала домен, потом инфраструктура.
🔸Божественный агрегат
Агрегат начинает управлять большим количеством сущностей и содержит очень много логики.
101% нарушены границы транзакций. Исправляется разделением агрегатов на кластеры по бизнес-процессам.
🔸Божественный сервис
Модель находится в анемичном состоянии, сервис берет на себя всю логику. Смысл модели пропадает, код становится непредсказуемым.
Важно: вся логика должна быть в агрегатах, сущностях и VO.
🔸Отсутствие Value Objects
В сущностях, вместо VO используются примитивные типы.
Сильно усложняет понимание кода, увеличивается вероятность простых ошибок.
🔸Нарушение границ контекстов
Смешивание моделей из разных контекстов.
Изменения в одном контексте ломают другой.
🔸Отсутствие тестов и изоляции в доменном слое
Про тесты это больше моё правило, домен должен быть весь покрыт тестами. А также, в домене не должно быть никаких инфраструктурных зависимостей (от БД или фреймворка).
#DDD
🔸Анемичная модель (Anemic Domain Model)
Сущность не содержит никакой бизнес-логики (методов). По сути, сущность используется как DTO.
Это влечет за собой нарушение инкапсуляции, логика уезжает на прикладной уровень.
🔸ORM-анемия
Это частный случай анемичной модели, когда ORM (таблица) диктует структуру доменного объекта.
type OrderModel struct {
ID string `gorm:"primaryKey"`
Status string
}Кроме того, это нарушает SRP. Используем DTO для маппинга агрегата в репозитории.
Важно: сначала домен, потом инфраструктура.
🔸Божественный агрегат
Агрегат начинает управлять большим количеством сущностей и содержит очень много логики.
type User struct {
ID uuid.UUID
Orders []Order
Payments []Payment
Addresses []Address
}101% нарушены границы транзакций. Исправляется разделением агрегатов на кластеры по бизнес-процессам.
🔸Божественный сервис
Модель находится в анемичном состоянии, сервис берет на себя всю логику. Смысл модели пропадает, код становится непредсказуемым.
func (s *OrderService) CancelOrder(orderID uuid.UUID) error {
order, _ := s.repo.FindByID(orderID)
if order.Status == "paid" { // Валидация в сервисе
return errors.New("нельзя отменить оплаченный заказ")
}
order.Status = "cancelled"
return s.repo.Save(order)
}Важно: вся логика должна быть в агрегатах, сущностях и VO.
🔸Отсутствие Value Objects
В сущностях, вместо VO используются примитивные типы.
type OrderModel struct {
ID string
Status string
}Сильно усложняет понимание кода, увеличивается вероятность простых ошибок.
🔸Нарушение границ контекстов
Смешивание моделей из разных контекстов.
Изменения в одном контексте ломают другой.
// Контекст "Заказы" зависит от "Каталог"
type Order struct {
Product Product // Модель из другого контекста
}
// Использем ID вместо объектов
type Order struct {
ProductID uuid.UUID
}
🔸Отсутствие тестов и изоляции в доменном слое
Про тесты это больше моё правило, домен должен быть весь покрыт тестами. А также, в домене не должно быть никаких инфраструктурных зависимостей (от БД или фреймворка).
#DDD
5🫡2
Тактические паттерны DDD: Repository
Репозиторий инкапсулирует в себе логику хранения агрегата в базе данных, а так же:
- Изолирует домен от деталей инфраструктуры.
- Упрощает разработку и тестирование. Используя моки можно отложить принятие решения о БД в самый конец.
🔸Интерфейс репозитория может находиться на уровне домена, если его нужно будет использовать в доменных сервисах. С одной стороны можно замокать интерфейс в одном месте сразу со всеми методами, а с другой такой подход противоречит принципам golang: интерфейс определяется там где используется.
🔸Главная функция репозитория - работа с одним агрегатом. Включая вложенные сущности. Т.е. соблюдение транзакционной целостности.
🔸Методы репозитория принимают и возвращают агрегаты, но при этом внутри метода должны использоваться DTO для трансляции агрегатов в поля БД.
🔸Ненужно стараться использовать универсальные методы в репозитории. Метод FindActiveUsers вполне подходит.
🔸Репозиторий должен возвращать ошибки, понятные в домене. Например ErrOrderNotFound, вместо sql.ErrNoRows.
#DDD
Репозиторий инкапсулирует в себе логику хранения агрегата в базе данных, а так же:
- Изолирует домен от деталей инфраструктуры.
- Упрощает разработку и тестирование. Используя моки можно отложить принятие решения о БД в самый конец.
type OrderRepository interface {
Load(context.Contex, OrderID) (*Order, error)
Save(context.Context, Order) error
Delete(context.Context, OrderID) error
}🔸Интерфейс репозитория может находиться на уровне домена, если его нужно будет использовать в доменных сервисах. С одной стороны можно замокать интерфейс в одном месте сразу со всеми методами, а с другой такой подход противоречит принципам golang: интерфейс определяется там где используется.
🔸Главная функция репозитория - работа с одним агрегатом. Включая вложенные сущности. Т.е. соблюдение транзакционной целостности.
🔸Методы репозитория принимают и возвращают агрегаты, но при этом внутри метода должны использоваться DTO для трансляции агрегатов в поля БД.
type OrderDTO struct{}
func (o *OrderDTO) ToEntity() *model.Order {
return &model.Order{}
}
func NewRow(order model.Order) OrderDTO {
}🔸Ненужно стараться использовать универсальные методы в репозитории. Метод FindActiveUsers вполне подходит.
🔸Репозиторий должен возвращать ошибки, понятные в домене. Например ErrOrderNotFound, вместо sql.ErrNoRows.
#DDD
👍2🫡1
Паттерн Unit of Work
Это паттерн проектирования, который группирует несколько связанных операций с данными в одну атомарную транзакцию. Все изменения будут или сохранены или отменены как единое целое.
Задача: нужно атомарно изменить данные двух агрегатов.
🔸Реализация 1: в лоб, в методе апликейшен сервиса создаем транзакцию и используя ее выполняем действия в двух репах.
🔸Реализация 2: изолируем изменения в UoW.
Интерфейс UoW объекта относится к уровню приложения (application):
Пример одного из репозиториев:
И сам Unit of Work (относится к инфраструктурному слою):
Ну и пример использования в методе application service:
🔸Несколько правил:
- UoW может работать в application или domain сервисах.
- Не должен быть глобальным, божественным UoW. Лучше ограничить каким-то контекстом операции (PaymentUnitOfWork, UserUnitOfWork, OrderTransactionManager).
- Не забывайте что долгие транзакции блокируют ресурсы.
🔸Saga используется для распределенных транзакций, тогда как UoW - для локальных в рамках одной БД.
🔸Как размазать по слоям UoW в гексагональной архитектуре:
#patterns
Это паттерн проектирования, который группирует несколько связанных операций с данными в одну атомарную транзакцию. Все изменения будут или сохранены или отменены как единое целое.
Задача: нужно атомарно изменить данные двух агрегатов.
🔸Реализация 1: в лоб, в методе апликейшен сервиса создаем транзакцию и используя ее выполняем действия в двух репах.
🔸Реализация 2: изолируем изменения в UoW.
Интерфейс UoW объекта относится к уровню приложения (application):
type CreateOrderUnitOfWork interface {
Begin(ctx context.Context) error
Commit() error
Rollback() error
// Репозитории, управляемые UoW
OrderRepository() OrderRepository
ProductRepository() ProductRepository
}Пример одного из репозиториев:
type OrderRepository struct {
tx *sql.Tx
}
func NewOrderRepository(tx *sql.Tx) *OrderRepository {
return &OrderRepository{
tx: tx,
}
}
func (r *OrderRepository) Save(ctx context.Context, order *Order) error {
_, err := r.tx.ExecContext(ctx, "INSERT INTO orders (id, customer_id, total_amount) VALUES (?, ?, ?)", order.ID, order.CustomerID, order.TotalAmount)
return err
}И сам Unit of Work (относится к инфраструктурному слою):
type CreateOrderUnitOfWork struct {
db *sql.DB
tx *sql.Tx
orderRepository OrderRepository
productRepository ProductRepository
}
func NewCreateOrderUnitOfWork(db *sql.DB) *CreateOrderUnitOfWork {
return &CreateOrderUnitOfWork{
db: db,
}
}
func (u *CreateOrderUnitOfWork) Begin(ctx context.Context) error {
tx, err := u.db.BeginTx(ctx, nil)
if err != nil {
return err
}
u.tx = tx
u.orderRepository = NewOrderRepository(tx)
u.productRepository = NewProductRepository(tx)
return nil
}
func (u *CreateOrderUnitOfWork) Commit() error {
return u.tx.Commit()
}
func (u *CreateOrderUnitOfWork) Rollback() error {
return u.tx.Rollback()
}
func (u *CreateOrderUnitOfWork) OrderRepository() OrderRepository {
return u.orderRepository
}
func (u *CreateOrderUnitOfWork) ProductRepository() ProductRepository {
return u.productRepository
}Ну и пример использования в методе application service:
type OrderService struct {
createOrder CreateOrderUnitOfWork
}
func (s *OrderService) CreateOrder(ctx context.Context, productID domain.ProductID) error {
if err := s.createOrder.Begin(ctx); err != nil {
return err
}
defer s.createOrder.Rollback()
product, err := s.createOrder.ProductRepository().FindByID(ctx, productID)
if err != nil {
return err
}
order := domain.NewOrder()
order.AddItem(product)
if err := s.createOrder.OrderRepository().Save(ctx, order); err != nil {
return err
}
return s.createOrder.Commit()
}🔸Несколько правил:
- UoW может работать в application или domain сервисах.
- Не должен быть глобальным, божественным UoW. Лучше ограничить каким-то контекстом операции (PaymentUnitOfWork, UserUnitOfWork, OrderTransactionManager).
- Не забывайте что долгие транзакции блокируют ресурсы.
🔸Saga используется для распределенных транзакций, тогда как UoW - для локальных в рамках одной БД.
🔸Как размазать по слоям UoW в гексагональной архитектуре:
├── domain/ # Доменный слой
│ ├── order.go # Агрегат Order
│ └── product.go # Агрегат Product
│
├── application/ # Прикладной слой
│ ├── ports/ # Порты (интерфейсы)
│ │ └── uow.go # Интерфейс UnitOfWork
│ └── services/ # Сервисы приложения
│ └── order_service.go # Use Case: оформление заказа
│
└── adapters/ # Инфраструктурный слой
└── persistence/
├── create_order_uow.go # Реализация CreateOrderUnitOfWork
└── postgre/ # Реализации репозиториев
└── order_repository.go
#patterns
👍5🫡2
Паттерн Strategy
Я хочу признаться: я не люблю if'ы, да и switch'и тоже :)
Часто бывает, что за if'ами скрывается разделение функциональности по какому-то типу. Например, отправка SMS и отправка сообщения в какую-то CRM. Или метод коммуникации: через SMS или посредством звонка.
А сам выбор типа коммуникации зависит от каких-то настроек (приложения или конкретного пользователя).
То есть, возможны две стратегии коммуникации: SMS или CRM. Так вот, паттерн стратегия предлагает изолировать конкретные стратегии в отдельные классы (структуры), а выше них ставить класс, который будет ими управлять.
Как мы уже знаем, проектирование всегда идет от модели данных. В нашем случае это набор данных, по которым мы можем понять, кому, как и что нужно отправить.
Далее определяем интерфейс, который будет реализован стратегиями:
Реализуем этот интерфейс:
Ну и сам менеджер стратегий:
Таким образом, менеджер содержит набор всех возможных стратегий коммуникации. Их не обязательно задавать в фабричном методе - можно и передавать. Используя данные из модели, определяем стратегию и отправляем сообщение.
Взглянем немного шире: таким образом, мы поддержали Open-Close Principle.
Конечно сам выбор стратегии может быть сложнее - тогда мы просто изолируем выбор стратегии в отдельный метод менеджера.
Дальше будет еще несколько постов про самые важные, на мой взгляд, паттерны.
#patterns
Я хочу признаться: я не люблю if'ы, да и switch'и тоже :)
Часто бывает, что за if'ами скрывается разделение функциональности по какому-то типу. Например, отправка SMS и отправка сообщения в какую-то CRM. Или метод коммуникации: через SMS или посредством звонка.
А сам выбор типа коммуникации зависит от каких-то настроек (приложения или конкретного пользователя).
То есть, возможны две стратегии коммуникации: SMS или CRM. Так вот, паттерн стратегия предлагает изолировать конкретные стратегии в отдельные классы (структуры), а выше них ставить класс, который будет ими управлять.
Как мы уже знаем, проектирование всегда идет от модели данных. В нашем случае это набор данных, по которым мы можем понять, кому, как и что нужно отправить.
type CommunicationStrategy string
const (
SMS CommunicationStrategy = "SMS"
AmoCRM CommunicationStrategy = "AmoCRM"
)
type Message struct {
From string
To string
Text string
CommunicateVIA CommunicationStrategy
}
Далее определяем интерфейс, который будет реализован стратегиями:
type Communitator interface {
SendMessage(context.Context, Message) error
}Реализуем этот интерфейс:
type SMS struct {
}
func (c *SMS) SendMessage(ctx context.Context, msg Message) error {
// подключение к SMS-сервису
// отправка SMS
return nil
}
type AmoCRM struct {
}
func (c *AmoCRM) SendMessage(ctx context.Context, msg Message) error {
// работа с API
return nil
}Ну и сам менеджер стратегий:
type CommunicationManager struct {
strategies map[CommunicationStrategy]Communitator
}
func NewCommunicationManager() *CommunicationManager {
return &CommunicationManager{
strategies: map[CommunicationStrategy]Communitator{
SMS: &SMS{},
AmoCRM: &AmoCRM{},
},
}
}
func (m *CommunicationManager) SendMessage(ctx context.Context, msg Message) error {
if communitator, ok := m.strategies[msg.CommunicateVIA]; ok {
return communitator.SendMessage(ctx, msg)
}
return fmt.Errorf("unsupported communication strategy: %s", msg.CommunicateVIA)
}Таким образом, менеджер содержит набор всех возможных стратегий коммуникации. Их не обязательно задавать в фабричном методе - можно и передавать. Используя данные из модели, определяем стратегию и отправляем сообщение.
Взглянем немного шире: таким образом, мы поддержали Open-Close Principle.
Конечно сам выбор стратегии может быть сложнее - тогда мы просто изолируем выбор стратегии в отдельный метод менеджера.
Дальше будет еще несколько постов про самые важные, на мой взгляд, паттерны.
#patterns
👏5👍2🫡2
Паттерн Specification
Этот паттерн помогает выразить сложные бизнес-правила (критерии) через отдельные, переиспользуемые объекты.
Пример правил:
- Пользователь должен быть старше 18 лет.
- Пользователь должен быть активным.
- Пользователь должен иметь роль покупателя.
Решение в лоб: наделать кучу ифов.
Решение по уму: паттерн Specification, где
- Выносим каждое правило в отдельный объект.
- Легко тестировать.
- Легко комбинировать друг с другом.
Для начала определим интерфейс проверки правил:
Добавим пару спецификаций:
И их комбинацию:
OR-спецификация выглядит так:
Проверяем работу:
Когда использовать Specification:
- Выражение сложных бизнес-правил в коде.
- Когда бизнес-правила часто меняются.
- Когда нужно переиспользовать правила в разный частях приложения.
#patterns
Этот паттерн помогает выразить сложные бизнес-правила (критерии) через отдельные, переиспользуемые объекты.
Пример правил:
- Пользователь должен быть старше 18 лет.
- Пользователь должен быть активным.
- Пользователь должен иметь роль покупателя.
type User struct {
Age int
IsActive bool
}Решение в лоб: наделать кучу ифов.
Решение по уму: паттерн Specification, где
- Выносим каждое правило в отдельный объект.
- Легко тестировать.
- Легко комбинировать друг с другом.
Для начала определим интерфейс проверки правил:
type Specification[T any] interface {
IsValid(candidate T) bool
}Добавим пару спецификаций:
type AgeSpecification struct {
minAge int
}
func NewAgeSpecification(minAge int) *AgeSpecification {
return &AgeSpecification{minAge: minAge}
}
func (s *AgeSpecification) IsValid(candidate User) bool {
return candidate.Age >= s.minAge
}
type ActiveSpecification struct{}
func NewActiveSpecification() *ActiveSpecification {
return &ActiveSpecification{}
}
func (s *ActiveSpecification) IsValid(candidate User) bool {
return candidate.IsActive
}И их комбинацию:
type AndSpecification[T any] struct {
specifications []Specification[T]
}
func NewAndSpecification[T any](specifications ...Specification[T]) *AndSpecification[T] {
return &AndSpecification[T]{specifications: specifications}
}
func (s *AndSpecification[T]) IsValid(candidate T) bool {
for _, specification := range s.specifications {
if !specification.IsValid(candidate) {
return false
}
}
return true
}OR-спецификация выглядит так:
func (s *OrSpecification) IsValid(candidate T) bool {
for _, specification := range s.specifications {
if specification.IsValid(product) {
return true
}
}
return false
}Проверяем работу:
func main() {
user := User{Age: 20, IsActive: false}
andSpec := NewAndSpecification(
NewAgeSpecification(18),
NewActiveSpecification(),
)
fmt.Println(andSpec.IsValid(user))
}Когда использовать Specification:
- Выражение сложных бизнес-правил в коде.
- Когда бизнес-правила часто меняются.
- Когда нужно переиспользовать правила в разный частях приложения.
#patterns
👍4🫡2
Паттерн Outbox
Паттерн обеспечивает атомарность при работе с БД и брокерами сообщений.
Пример 1: Идет транзакция изменения агрегата. И на каком-то шаге нужно отправить ивент в Kafka, отправляем ивент, а на следующем шаге транзакция падает с ошибкой. Но ивент уже отправлен.
Пример 2: Консюмер читает ивенты из Kafka и выполняет действия. Вопрос: как обеспечить идемпотентность?
Основная идея:
🔸Сохраняем событие в таблицу outbox в рамках той же транзакции, что и изменение данных агрегата.
🔸Фоновым процессом читаем эту таблицу и отправляем события в брокер.
🔸После успешной отправки или меняем статус события, или удаляем из таблицы.
Пример таблицы:
Далее добавляем сущность OrderEvent и репозиторий ее сохранения.
Фоновый процесс:
В простом случае - удаляем, но можем и менять статус. А также можно считать количество попыток, добавить метрики, подумать про лочки.
Как обработать событие со стороны консюмера?
Точно также: сначала кладем в БД, потом фоновым процессом вычитываем и обрабатываем.
Обратите внимание на EventID, это наша идемпотентность.
Итого
Паттерн отлично подходит для агрегатов и доменных событий. Хорошо ложится в CQRS и Unit of Work.
#patterns #DDD
Паттерн обеспечивает атомарность при работе с БД и брокерами сообщений.
Пример 1: Идет транзакция изменения агрегата. И на каком-то шаге нужно отправить ивент в Kafka, отправляем ивент, а на следующем шаге транзакция падает с ошибкой. Но ивент уже отправлен.
Пример 2: Консюмер читает ивенты из Kafka и выполняет действия. Вопрос: как обеспечить идемпотентность?
Основная идея:
🔸Сохраняем событие в таблицу outbox в рамках той же транзакции, что и изменение данных агрегата.
🔸Фоновым процессом читаем эту таблицу и отправляем события в брокер.
🔸После успешной отправки или меняем статус события, или удаляем из таблицы.
Пример таблицы:
CREATE TABLE outbox (
id UUID PRIMARY KEY,
event_id VARCHAR(255) NOT NULL,
event_type VARCHAR(255) NOT NULL,
payload JSONB NOT NULL,
status VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
Далее добавляем сущность OrderEvent и репозиторий ее сохранения.
Фоновый процесс:
func (w *OutboxWorker) Start(ctx context.Context) {
ticker := time.NewTicker(w.pollInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
events, _ := w.outboxRepo.FetchPending(ctx, 100)
for _, event := range events {
if err := w.producer.Send(event); err != nil {
continue
}
_ = w.outboxRepo.Delete(ctx, event.ID)
}
case <-ctx.Done():
return
}
}
}В простом случае - удаляем, но можем и менять статус. А также можно считать количество попыток, добавить метрики, подумать про лочки.
Как обработать событие со стороны консюмера?
Точно также: сначала кладем в БД, потом фоновым процессом вычитываем и обрабатываем.
Обратите внимание на EventID, это наша идемпотентность.
Итого
Паттерн отлично подходит для агрегатов и доменных событий. Хорошо ложится в CQRS и Unit of Work.
#patterns #DDD
👍5👏3🫡3
Паттерн Saga
Мы уже говорили о паттерне Unit of Work, когда у нас есть действия, которые надо выполнить атомарно в рамках одной БД.
Но, что если у нас микросервисы и нам нужно сделать последовательно несколько изменений в разных агрегатах, да еще и в разных ограниченных контекстах (читай микросервисах)?
Суть паттерна:
🔸Транзакции выполняются локально в каждом сервисе.
🔸По окончании транзакции генерируется событие для следующего сервиса.
🔸Если какая-либо транзакция заканчивается ошибкой, нужно последовательно выполнить компенсирующие действия, чтобы отменить все изменения.
Типы Saga:
🔸Оркестрация
Вся власть сосредоточена в одних руках: центральный оркестратор управляет выполнением шагов. А также умеет вызывать компенсирующие действия.
То есть по сути, оркестратор читает события и вызывает команды микросервисов.
Этот тип саги хорош тем, что в нем есть четкий контроль потока и легкая отладка, но при это имеется центральная точка отказа.
🔸Хореография
В этом случае ответственность за распределенную транзакцию ложится на каждый микросервис, участвующий в ней.
Суть та же самая: первый микросервис выполняет часть своей транзакции и отправляем ивент. Второй микросервис читает этот ивент и выполняет следующий шаг. Если возникает ошибка: микросервис выбрасывает ивент, предыдущий выполняет компенсацию и также выбрасывает ивент и т.д.
Этот тип саги хорош тем, что в нем есть высокая масштабируемость и слабая связность, но сложная отладка и отслеживание.
Связь с DDD
🔸При изменении агрегата генерируется событие, которое и запускает шаги саги.
🔸Unit of Work выполняет отдельный шаг саги. В этом ему помогают паттерны Repository и Outbox.
Сложности
🔸Отсутствие идемпотентности. Как это исправлять читаем предыдущий пост Outbox.
🔸Бесконечная компенсация. А что будет если компенсирующие действие также завершится ошибкой? Смотрим в сторону кол-ва попыток и алертов.
#patterns #DDD
Мы уже говорили о паттерне Unit of Work, когда у нас есть действия, которые надо выполнить атомарно в рамках одной БД.
Но, что если у нас микросервисы и нам нужно сделать последовательно несколько изменений в разных агрегатах, да еще и в разных ограниченных контекстах (читай микросервисах)?
Суть паттерна:
🔸Транзакции выполняются локально в каждом сервисе.
🔸По окончании транзакции генерируется событие для следующего сервиса.
🔸Если какая-либо транзакция заканчивается ошибкой, нужно последовательно выполнить компенсирующие действия, чтобы отменить все изменения.
Типы Saga:
🔸Оркестрация
Вся власть сосредоточена в одних руках: центральный оркестратор управляет выполнением шагов. А также умеет вызывать компенсирующие действия.
То есть по сути, оркестратор читает события и вызывает команды микросервисов.
Этот тип саги хорош тем, что в нем есть четкий контроль потока и легкая отладка, но при это имеется центральная точка отказа.
🔸Хореография
В этом случае ответственность за распределенную транзакцию ложится на каждый микросервис, участвующий в ней.
Суть та же самая: первый микросервис выполняет часть своей транзакции и отправляем ивент. Второй микросервис читает этот ивент и выполняет следующий шаг. Если возникает ошибка: микросервис выбрасывает ивент, предыдущий выполняет компенсацию и также выбрасывает ивент и т.д.
Этот тип саги хорош тем, что в нем есть высокая масштабируемость и слабая связность, но сложная отладка и отслеживание.
Связь с DDD
🔸При изменении агрегата генерируется событие, которое и запускает шаги саги.
🔸Unit of Work выполняет отдельный шаг саги. В этом ему помогают паттерны Repository и Outbox.
Сложности
🔸Отсутствие идемпотентности. Как это исправлять читаем предыдущий пост Outbox.
🔸Бесконечная компенсация. А что будет если компенсирующие действие также завершится ошибкой? Смотрим в сторону кол-ва попыток и алертов.
#patterns #DDD
Telegram
От сердца к коду
Паттерн Unit of Work
Это паттерн проектирования, который группирует несколько связанных операций с данными в одну атомарную транзакцию. Все изменения будут или сохранены или отменены как единое целое.
Задача: нужно атомарно изменить данные двух агрегатов.…
Это паттерн проектирования, который группирует несколько связанных операций с данными в одну атомарную транзакцию. Все изменения будут или сохранены или отменены как единое целое.
Задача: нужно атомарно изменить данные двух агрегатов.…
🔥5🫡2
Месяц ничего не выкладывал, сначала был отпуск, потом лень. Каюсь.
Пришло время вернуться к регулярным постам 2-3 раза в неделю. Также будем продолжать разбирать архитектуру и паттерны, но хочу подмешать еще и менеджерские вопросы. Сам я уже какое-то время почти не пишу код, лет 6 работаю техлидом, а сейчас руководителем разработки, кажется есть что сказать по этому поводу.
Постараюсь не нудеть и не отходить далеко от проблем кода. Продолжаем...
Пришло время вернуться к регулярным постам 2-3 раза в неделю. Также будем продолжать разбирать архитектуру и паттерны, но хочу подмешать еще и менеджерские вопросы. Сам я уже какое-то время почти не пишу код, лет 6 работаю техлидом, а сейчас руководителем разработки, кажется есть что сказать по этому поводу.
Постараюсь не нудеть и не отходить далеко от проблем кода. Продолжаем...
❤13🫡3
Роль техлида в команде разработки
Да, честно сказать в разных компаниях эта роль означает разное. Где-то были уже выстроенные процессы, нужно было просто в них влиться и отвечать за качество разработки. Где-то процессов или не было или они были плохими и приходилось заниматься выстраиванием процессов, а потом уже качеством разработки. Т.е. в одном месте перевес может быть в менеджерскую сторону, а в другом в техническую.
В целом лид должен уметь и то и то, но не просто уметь, а иметь опыт и понимать как процессы и качество отражаются на команде. С одной стороны хорошие технические навыки: понятие качественного кода, архитектуры приложений. А с другой стороны: высокие софт скиллы, скиллы по фасилитации, стратегически понимать куда идет команда и как ее туда довести.
Недавно, в одной из книг прочел, что лид служит своей команде, еще раз: лид работает для команды, а не наоборот.
🔸А теперь лично мой чеклист для лида:
- Понимание ритуалов Agile/Scrum
- Работа с непрерывным обучением команды, выращивание себе замены
- Распределение задач с учетом навыком, интересов и нагрузки
- Умение правильно делегировать задачи
- Замер температуры: проведение 1 на 1
- Создание психологически комфортной среды в команде
- Участие в найме
- Работа с метриками
- Коммуникации
Технически:
- Архитектура и дизайн
- Работа с техдолгом и багами
- Код-ревью и стандарты повышения качества
- Финальное слово в спорных технических вопросах
Как будто бы менеджерских навыков надо побольше?) Наверное, так и есть.
🔸А теперь давайте обсудим еще несколько вопросов кратко.
Надо ли писать код? Лично я уверен что надо, скажем так, 20-30% времени. Не забивайте на это дело! Если перестать писать код совсем - можно быстро обесцениться.
Как из хорошего разработчика сделать плохого менеджера?) Да-да, сделать его лидом.
Зачем оно тебе? Ну вот пришел в компанию с плохими процессами, а у тебя есть опыт работы в хороших. Если ты просто разработчик - у тебя нет влияния на процессы, а очень хочется.
Лидство на интуиции. Бывает такое, что у человека высокие софт скиллы сами по себе. Он интуитивно понимает потребности и направления команды. Достаточно ли этого, чтобы стать хорошим лидом? - Нет. Интуицию надо дополнять теорией, а теорию практикой.
🔸Работа лида измеряется работой его команды. Не забываем про метрики:
- снижение числа инцедентов, скорость доставки фич
- кол-во багов в проде
- процент времени, который разработчики именно пишут код
- рост уровня разработки
- выполнение планов релизов
🔸И на последок, как всегда, несколько книг для начала:
- Мама, я тимлид! Практические советы по руководству IT-командой
- Карьера Software Engineering Manager. Эффективное управление командой разработчиков ПО
- Человеческий фактор. Успешные проекты и команды
- Проект "Феникс"
- Идеальный руководитель: Почему им нельзя стать и что из этого следует
По менеджменту в IT, и не только, книг огромное количество, выше я указал лишь те, что больше мне запомнились.
#management #teamlead
Да, честно сказать в разных компаниях эта роль означает разное. Где-то были уже выстроенные процессы, нужно было просто в них влиться и отвечать за качество разработки. Где-то процессов или не было или они были плохими и приходилось заниматься выстраиванием процессов, а потом уже качеством разработки. Т.е. в одном месте перевес может быть в менеджерскую сторону, а в другом в техническую.
В целом лид должен уметь и то и то, но не просто уметь, а иметь опыт и понимать как процессы и качество отражаются на команде. С одной стороны хорошие технические навыки: понятие качественного кода, архитектуры приложений. А с другой стороны: высокие софт скиллы, скиллы по фасилитации, стратегически понимать куда идет команда и как ее туда довести.
Недавно, в одной из книг прочел, что лид служит своей команде, еще раз: лид работает для команды, а не наоборот.
🔸А теперь лично мой чеклист для лида:
- Понимание ритуалов Agile/Scrum
- Работа с непрерывным обучением команды, выращивание себе замены
- Распределение задач с учетом навыком, интересов и нагрузки
- Умение правильно делегировать задачи
- Замер температуры: проведение 1 на 1
- Создание психологически комфортной среды в команде
- Участие в найме
- Работа с метриками
- Коммуникации
Технически:
- Архитектура и дизайн
- Работа с техдолгом и багами
- Код-ревью и стандарты повышения качества
- Финальное слово в спорных технических вопросах
Как будто бы менеджерских навыков надо побольше?) Наверное, так и есть.
🔸А теперь давайте обсудим еще несколько вопросов кратко.
Надо ли писать код? Лично я уверен что надо, скажем так, 20-30% времени. Не забивайте на это дело! Если перестать писать код совсем - можно быстро обесцениться.
Как из хорошего разработчика сделать плохого менеджера?) Да-да, сделать его лидом.
Зачем оно тебе? Ну вот пришел в компанию с плохими процессами, а у тебя есть опыт работы в хороших. Если ты просто разработчик - у тебя нет влияния на процессы, а очень хочется.
Лидство на интуиции. Бывает такое, что у человека высокие софт скиллы сами по себе. Он интуитивно понимает потребности и направления команды. Достаточно ли этого, чтобы стать хорошим лидом? - Нет. Интуицию надо дополнять теорией, а теорию практикой.
🔸Работа лида измеряется работой его команды. Не забываем про метрики:
- снижение числа инцедентов, скорость доставки фич
- кол-во багов в проде
- процент времени, который разработчики именно пишут код
- рост уровня разработки
- выполнение планов релизов
🔸И на последок, как всегда, несколько книг для начала:
- Мама, я тимлид! Практические советы по руководству IT-командой
- Карьера Software Engineering Manager. Эффективное управление командой разработчиков ПО
- Человеческий фактор. Успешные проекты и команды
- Проект "Феникс"
- Идеальный руководитель: Почему им нельзя стать и что из этого следует
По менеджменту в IT, и не только, книг огромное количество, выше я указал лишь те, что больше мне запомнились.
#management #teamlead
❤5🫡3👍2🤔2
Паттерн Policy
Продолжаем разбирать важные, на мой взгляд, паттерны. И сегодня поговорим про политику.
Для меня этот паттерн, по смыслу, напоминает стратегию, но немного проще. Он также позволяет инкапсулировать правила принятия решений, при этом не изменяя текущего кода. Например, расчет скидок, выбор тарифа. Стратегия кажется сложнее, там взаимозаменяемы алгоритмы, изменение данных и т.п.
🔸Итого, политики:
- Не должны быть большими и сложными.
- Не должны изменять состояния системы, только возвращать результат.
- Не должны иметь привязку к инфраструктуре.
🔸Перейдем к коду:
Далее используем политики в аппликейшен сервисе:
🔸Strategy vs Policy vs Specification:
- В Strategy ключевая идея: выбор алгоритма или способа выполнения чего-то. Фокус на том, как что-то делается. Например: выбор способа коммуникации, выбор способа сортировки.
- Specification: определение критериев соответствия. Фокус на что отбирать или проверять (критерии). Например, валидация пользователей, фильтрация списка продуктов по цене, категории и т.п.
- Policy: определение правил или ограничений, которым должен соответствовать объект или процесс. Фокус на что разрешено или запрещено. Например: политика безопасности доступа к ресурсам в зависимости от роли пользователя.
#patterns
Продолжаем разбирать важные, на мой взгляд, паттерны. И сегодня поговорим про политику.
Для меня этот паттерн, по смыслу, напоминает стратегию, но немного проще. Он также позволяет инкапсулировать правила принятия решений, при этом не изменяя текущего кода. Например, расчет скидок, выбор тарифа. Стратегия кажется сложнее, там взаимозаменяемы алгоритмы, изменение данных и т.п.
🔸Итого, политики:
- Не должны быть большими и сложными.
- Не должны изменять состояния системы, только возвращать результат.
- Не должны иметь привязку к инфраструктуре.
🔸Перейдем к коду:
// Интерфейс политики
type DiscountPolicy interface {
CalculateDiscount(ctx context.Context, customer Customer, order Order) Discount
}
// Примеры политик
type VIPDiscountPolicy struct{}
func (p VIPDiscountPolicy) CalculateDiscount(ctx context.Context, customer Customer, _ Order) Discount {
if customer.IsVIP {
return 0.2 // 20%
}
return 0
}
type NewYearDiscountPolicy struct{}
func (p NewYearDiscountPolicy) CalculateDiscount(ctx context.Context, _ Customer, _ Order) Discount {
if time.Now().Month() == time.December {
return 0.25 // 25%
}
return 0
}
// Объединение политик
type BestOfferDiscountPolicy struct {
policies []DiscountPolicy
}
func NewBestOfferDiscountPolicy(policies ...DiscountPolicy) *BestOfferDiscountPolicy {
return &BestOfferDiscountPolicy{policies: policies}
}
func (p *BestOfferDiscountPolicy) CalculateDiscount(ctx context.Context, customer Customer, order Order) Discount {
bestDiscount := Discount(0)
for _, policy := range p.policies {
discount := policy.CalculateDiscount(ctx, customer, order)
if discount > bestDiscount {
bestDiscount = discount
}
}
return bestDiscount
}
Далее используем политики в аппликейшен сервисе:
type OrderService struct {
discountPolicy DiscountPolicy
}
func NewOrderService(policy DiscountPolicy) *OrderService {
return &OrderService{discountPolicy: policy}
}
func (s *OrderService) CalculateTotal(ctx context.Context, customer Customer, order Order) float64 {
discount := s.discountPolicy.CalculateDiscount(ctx, customer, order)
total := order.Subtotal * (1 - discount)
return total
}🔸Strategy vs Policy vs Specification:
- В Strategy ключевая идея: выбор алгоритма или способа выполнения чего-то. Фокус на том, как что-то делается. Например: выбор способа коммуникации, выбор способа сортировки.
- Specification: определение критериев соответствия. Фокус на что отбирать или проверять (критерии). Например, валидация пользователей, фильтрация списка продуктов по цене, категории и т.п.
- Policy: определение правил или ограничений, которым должен соответствовать объект или процесс. Фокус на что разрешено или запрещено. Например: политика безопасности доступа к ресурсам в зависимости от роли пользователя.
#patterns
Telegram
От сердца к коду
Паттерн Strategy
Я хочу признаться: я не люблю if'ы, да и switch'и тоже :)
Часто бывает, что за if'ами скрывается разделение функциональности по какому-то типу. Например, отправка SMS и отправка сообщения в какую-то CRM. Или метод коммуникации: через SMS…
Я хочу признаться: я не люблю if'ы, да и switch'и тоже :)
Часто бывает, что за if'ами скрывается разделение функциональности по какому-то типу. Например, отправка SMS и отправка сообщения в какую-то CRM. Или метод коммуникации: через SMS…
❤3🫡2👍1
Теория ограничений (ТОС) - Элияху Голдратт
🔸 Что это такое?
Теория, которая фокусируется на выявлении и устранений узких мест (ботл неков) в системе для повышения ее эффективности.
Если мы говорим про разработку: ТОС должна помогать оптимизировать процесс доставки продукта, сократить время разработки и улучшить прогнозируемость.
🔸 Борьба с горлышками
Пять шагов фокуса:
1. Найти ограничение - определить что тормозит процесс.
2. Эксплуатировать ограничение - работать в сторону эффективного использования ресурсов (например, освободить SRE от рутины).
3. Подчинить всё остальное ограничению - выстроить процессы так, чтобы ограничение не простаивало (например, отдать в разработку автоматизацию рутинных задач для SRE).
4. Расширить ограничение - увеличить мощность узкого места (нанять больше SRE)
5. Вернуться к шагу 1
Показатели эффективности:
- Пропускная способность — скорость доставки ценности клиенту
- Запасы — незавершённые задачи (например, накопление неоттестированного кода)
- Операционные расходы — затраты на поддержание процессов
- Время цикла (Cycle Time) — как быстро задачи проходят весь процесс
- Простой ресурсов — например, разработчики ждут тестирования
Далее приведу пару проблем и их решение.
🔸 Впихнуть невпихуемое
Ситуация: в спринт включают 20 задач, хотя команда стабильно делает 10. Тогда:
Последствия:
- Накопление незавершённой работы
- Падение качества:
- Растёт количество багов
- Упрощаются код-ревью (или они вообще пропускаются)
- Команда перестаёт рефакторить технический долг.
- Снижение пропускной способности (парадокс):
- Контекстное переключение между задачами «съедает» время
- Ошибки из-за спешки требуют переделок
- Выгорание команды
- Потеря прогнозируемости
Что же делать?
- Ограничение — пропускная способность команды (10 задач/спринт). Брать строго 10 задач всегда!
- Буфер: оставить 20% времени на непредвиденные работы
- Убрать многозадачность, фокус на завершении задач.
🔸 Оценка +15%
Ситуация: на каждую задачу всегда добавляется +15% времени в разработке.
Последствия:
- Увеличение времени цикла (Cycle Time) (заполнения выделенного времени)
- Рост незавершённой работы: накопление задач с завышенной оценкой создаёт иллюзию «загруженности», но реальная пропускная способность (throughput) не увеличивается.
- Маскировка ограничений: локальные запасы мешают выявить реальные узкие места
Когда запас допустим?
- Глобальные буферы: выделить 20% времени спринта на непредвиденные работы
- Учёт вариативности через статистику: Использовать исторические данные, чтобы планировать сроки, а не «накидывать» проценты на каждую задачу.
🔸Метод DBR (Drum-Buffer-Rope)
это подход из Теории ограничений, который синхронизирует работу системы с её «узким местом»:
- Drum (Барабан) — ритм работы ограничения (самого медленного этапа в системе).
- Buffer (Буфер) — защитный запас задач перед ограничением, чтобы оно не простаивало.
- Rope (Верёвка) — механизм, который «привязывает» запуск новых задач к скорости ограничения, предотвращая перегрузку
🔸 Что почитать?
- Цель
- Проект феникс
- Джедайские техники
#management #teamlead
🔸 Что это такое?
Теория, которая фокусируется на выявлении и устранений узких мест (ботл неков) в системе для повышения ее эффективности.
Если мы говорим про разработку: ТОС должна помогать оптимизировать процесс доставки продукта, сократить время разработки и улучшить прогнозируемость.
🔸 Борьба с горлышками
Любые изменения, которые не исправляют горлышко - это иллюзия.
Пять шагов фокуса:
1. Найти ограничение - определить что тормозит процесс.
2. Эксплуатировать ограничение - работать в сторону эффективного использования ресурсов (например, освободить SRE от рутины).
3. Подчинить всё остальное ограничению - выстроить процессы так, чтобы ограничение не простаивало (например, отдать в разработку автоматизацию рутинных задач для SRE).
4. Расширить ограничение - увеличить мощность узкого места (нанять больше SRE)
5. Вернуться к шагу 1
Показатели эффективности:
- Пропускная способность — скорость доставки ценности клиенту
- Запасы — незавершённые задачи (например, накопление неоттестированного кода)
- Операционные расходы — затраты на поддержание процессов
- Время цикла (Cycle Time) — как быстро задачи проходят весь процесс
- Простой ресурсов — например, разработчики ждут тестирования
Далее приведу пару проблем и их решение.
🔸 Впихнуть невпихуемое
Ситуация: в спринт включают 20 задач, хотя команда стабильно делает 10. Тогда:
Последствия:
- Накопление незавершённой работы
- Падение качества:
- Растёт количество багов
- Упрощаются код-ревью (или они вообще пропускаются)
- Команда перестаёт рефакторить технический долг.
- Снижение пропускной способности (парадокс):
- Контекстное переключение между задачами «съедает» время
- Ошибки из-за спешки требуют переделок
- Выгорание команды
- Потеря прогнозируемости
Что же делать?
- Ограничение — пропускная способность команды (10 задач/спринт). Брать строго 10 задач всегда!
- Буфер: оставить 20% времени на непредвиденные работы
- Убрать многозадачность, фокус на завершении задач.
🔸 Оценка +15%
Ситуация: на каждую задачу всегда добавляется +15% времени в разработке.
Последствия:
- Увеличение времени цикла (Cycle Time) (заполнения выделенного времени)
- Рост незавершённой работы: накопление задач с завышенной оценкой создаёт иллюзию «загруженности», но реальная пропускная способность (throughput) не увеличивается.
- Маскировка ограничений: локальные запасы мешают выявить реальные узкие места
Когда запас допустим?
- Глобальные буферы: выделить 20% времени спринта на непредвиденные работы
- Учёт вариативности через статистику: Использовать исторические данные, чтобы планировать сроки, а не «накидывать» проценты на каждую задачу.
🔸Метод DBR (Drum-Buffer-Rope)
это подход из Теории ограничений, который синхронизирует работу системы с её «узким местом»:
- Drum (Барабан) — ритм работы ограничения (самого медленного этапа в системе).
- Buffer (Буфер) — защитный запас задач перед ограничением, чтобы оно не простаивало.
- Rope (Верёвка) — механизм, который «привязывает» запуск новых задач к скорости ограничения, предотвращая перегрузку
🔸 Что почитать?
- Цель
- Проект феникс
- Джедайские техники
#management #teamlead
👍2🫡2👌1
Паттерн API Gateway
Это обратный прокси-сервер, который служит единой точкой входа в микросервисной архитектуре.
Проблема: у нас есть несколько микросервисов со своим API (возможно на разных протоколах), чтобы начать их использовать мы должны знать все их адреса, в каждом иметь авторизацию и т.п.
Решение: разрабатываем отдельный сервис, который принимает все входящие API запросы и маршрутизирует их соответствующим микросервисам.
Основные функции, которые может выполнять API gateway:
- Маршрутизация (уже проговорили)
- Агрегация данных: объединения запросов и ответов к и от нескольких микросервисов
- Аутентификация: как пример, проверка jwt-токена
- Кеширование
- Логирование и метрики
- Ограничение RPS
- Трансформация протоколов: REST -> gRPC
Также gateway хорошо подходит для миграции с legacy кода, замена монолита микросервисами с сохранением старого API.
Недостатки этого решения:
- Единая точка отказа: gateway упал, вся система стала недоступной
- Бутылочное горлышка: весь трафик идет через один узел.
Последнее о чем хотелось бы поговорить, это сравнение API Gateway, Open Host Service(OHS) и Anti-Corruption Layer (ACL):
- API Gateway: когда нужно упростить доступ к нескольким микросервисам. Например, мобильное приложение.
- OHS: когда ваш контекст должны использовать соседние команды/сервисы. Например, сервис платежей предоставляет API для заказов.
- ACL: при интеграции с legacy-системой или внешним API, для защиты доменной модели.
Пример взаимодействия:
#patterns #DDD
Это обратный прокси-сервер, который служит единой точкой входа в микросервисной архитектуре.
Проблема: у нас есть несколько микросервисов со своим API (возможно на разных протоколах), чтобы начать их использовать мы должны знать все их адреса, в каждом иметь авторизацию и т.п.
Решение: разрабатываем отдельный сервис, который принимает все входящие API запросы и маршрутизирует их соответствующим микросервисам.
Основные функции, которые может выполнять API gateway:
- Маршрутизация (уже проговорили)
- Агрегация данных: объединения запросов и ответов к и от нескольких микросервисов
- Аутентификация: как пример, проверка jwt-токена
- Кеширование
- Логирование и метрики
- Ограничение RPS
- Трансформация протоколов: REST -> gRPC
Также gateway хорошо подходит для миграции с legacy кода, замена монолита микросервисами с сохранением старого API.
Недостатки этого решения:
- Единая точка отказа: gateway упал, вся система стала недоступной
- Бутылочное горлышка: весь трафик идет через один узел.
Последнее о чем хотелось бы поговорить, это сравнение API Gateway, Open Host Service(OHS) и Anti-Corruption Layer (ACL):
- API Gateway: когда нужно упростить доступ к нескольким микросервисам. Например, мобильное приложение.
- OHS: когда ваш контекст должны использовать соседние команды/сервисы. Например, сервис платежей предоставляет API для заказов.
- ACL: при интеграции с legacy-системой или внешним API, для защиты доменной модели.
Пример взаимодействия:
[ Клиент ]
↓ HTTP-запрос
[ API Gateway ] ← Аутентификация, кеширование
↓
[ Сервис A: OHS ] ← Стандартизированный API
↓
[ Сервис B: ACL ] ← Трансформация данных из старой системы
↓
[ Legacy System ]
#patterns #DDD
Telegram
От сердца к коду
Интеграция ограниченных контекстов (часть 2)
В прошлом посте мы разобрали, как быть с общими доменными моделями. В этом же рассмотрим несколько паттернов для ситуаций, когда один ограниченный контекст предоставляет сервис другому.
1️⃣ Anti-Corruption Layer…
В прошлом посте мы разобрали, как быть с общими доменными моделями. В этом же рассмотрим несколько паттернов для ситуаций, когда один ограниченный контекст предоставляет сервис другому.
1️⃣ Anti-Corruption Layer…
🔥3🫡3👍2
Когда мы растем
Здесь я имею ввиду - растем профессионально, как разработчик или менеджер.
Этот пост больше философский, моё имхо на тему.
Ниже я приведу некоторые характеристики правильного вектора своего развития.
1️⃣ Рост там, где тяжело
Очень сложно вырасти в комфорте. Лично для меня комфорт наступает примерно через 3-4 года работы в одном месте. Работа должна быть сложной, непонятной.
Приведу пример самого максимального роста из своего опыта: позвали работать лидом в команду, на новый язык программирования и абсолютно незнакомый мне домен. И через пару месяцев выставили KPI и решили меня уволить :) От KPI я сразу отказался и пошел искать работу, но в итоге, чудесным образом, я остался в компании, стал полноценным лидом, разобрался в домене и ЯП.
2️⃣ Команда
Сильная команда выращивает хороших разработчиков. Сошлюсь на книгу Человеческий фактор, там было исследование что 1 сеньор работает в 4 раза лучше мидла и в 10 раз лучше джуна. Из собственного опыта - примерно согласен.
Ищите команду с сеньорами, но это очень сложно сделать! Ибо, сеньорство в разных компаниях разное, и по своему опыту: чем сильнее руководство - тем сильнее разработка. И наоборот.
Относительно примера выше: я бы не затащил лидство+домен+яп, если бы меня не окружали крутые специалисты. Включая разработку, qa, а особенно ПО, который у меня там был (Кирилл, привет :).
3️⃣ Жопочасы
Как ни крути количество потраченного времени решает. Команда, как собственный тренер, подсказывает направление знаний. Если заниматься одному - придется потратить больше времени на блуждания.
Пример: я сейчас учусь играть на электрогитаре. Прямая зависимость на потраченное время. Если на упражнения на ПШ я трачу 10 минут в день, то и выхлопа будет на 10 минут в день (те кто понял, тот понял). И да, у меня конечно есть преподаватель.
4️⃣ Это невозможно
Такая, мысленная ловушка, когда тебе выпадает какая-то задача и ты сначала думаешь что ее невозможно выполнить. Сейчас, с опытом, вообще нет таких мыслей, есть понимание что сложные задачи требуют больше времени. И всё.
Пример: да, разработка любой вещи, сложной, которую ты никогда не делал. Просто сначала гуглишь, потом ИИ, потом начинаешь собирать все в кучу, дочитываешь доку. И в один день - херак, и всё работает. Проверено уже не 1 раз.
5️⃣ Постоянное обучение
Кто не развивается - тот лох :) Читаем книги, статьи. Смотрим видео, проходим курсы. Пристаем к ИИ с вопросами. Главное не забиваем на свои знания.
6️⃣ Рост незрим в моменте
В моменте ты думаешь, что тебе тяжело, возможно включается синдром самозванца. А спустя некоторое время, смотришь назад - а у тебя всё получилось. Разобрался и в DDD, и вроде как понял как выделять микросервисы, а еще и SDK какой-то напилил.
Наверное пока это всё, что я хотел сказать про рост: тяжело и страшно, крутая команда, постоянное обучение.
#разработка #management #философия
Здесь я имею ввиду - растем профессионально, как разработчик или менеджер.
Этот пост больше философский, моё имхо на тему.
Ниже я приведу некоторые характеристики правильного вектора своего развития.
1️⃣ Рост там, где тяжело
Очень сложно вырасти в комфорте. Лично для меня комфорт наступает примерно через 3-4 года работы в одном месте. Работа должна быть сложной, непонятной.
Приведу пример самого максимального роста из своего опыта: позвали работать лидом в команду, на новый язык программирования и абсолютно незнакомый мне домен. И через пару месяцев выставили KPI и решили меня уволить :) От KPI я сразу отказался и пошел искать работу, но в итоге, чудесным образом, я остался в компании, стал полноценным лидом, разобрался в домене и ЯП.
2️⃣ Команда
Сильная команда выращивает хороших разработчиков. Сошлюсь на книгу Человеческий фактор, там было исследование что 1 сеньор работает в 4 раза лучше мидла и в 10 раз лучше джуна. Из собственного опыта - примерно согласен.
Ищите команду с сеньорами, но это очень сложно сделать! Ибо, сеньорство в разных компаниях разное, и по своему опыту: чем сильнее руководство - тем сильнее разработка. И наоборот.
Относительно примера выше: я бы не затащил лидство+домен+яп, если бы меня не окружали крутые специалисты. Включая разработку, qa, а особенно ПО, который у меня там был (Кирилл, привет :).
3️⃣ Жопочасы
Как ни крути количество потраченного времени решает. Команда, как собственный тренер, подсказывает направление знаний. Если заниматься одному - придется потратить больше времени на блуждания.
Пример: я сейчас учусь играть на электрогитаре. Прямая зависимость на потраченное время. Если на упражнения на ПШ я трачу 10 минут в день, то и выхлопа будет на 10 минут в день (те кто понял, тот понял). И да, у меня конечно есть преподаватель.
4️⃣ Это невозможно
Такая, мысленная ловушка, когда тебе выпадает какая-то задача и ты сначала думаешь что ее невозможно выполнить. Сейчас, с опытом, вообще нет таких мыслей, есть понимание что сложные задачи требуют больше времени. И всё.
Пример: да, разработка любой вещи, сложной, которую ты никогда не делал. Просто сначала гуглишь, потом ИИ, потом начинаешь собирать все в кучу, дочитываешь доку. И в один день - херак, и всё работает. Проверено уже не 1 раз.
5️⃣ Постоянное обучение
Кто не развивается - тот лох :) Читаем книги, статьи. Смотрим видео, проходим курсы. Пристаем к ИИ с вопросами. Главное не забиваем на свои знания.
6️⃣ Рост незрим в моменте
В моменте ты думаешь, что тебе тяжело, возможно включается синдром самозванца. А спустя некоторое время, смотришь назад - а у тебя всё получилось. Разобрался и в DDD, и вроде как понял как выделять микросервисы, а еще и SDK какой-то напилил.
Наверное пока это всё, что я хотел сказать про рост: тяжело и страшно, крутая команда, постоянное обучение.
#разработка #management #философия
🔥8🫡4👍1😢1
Event Sourcing
Это паттерн проектирования, при котором состояние агрегата определяется цепочкой событий. Т.е. вместо сохранения состояние в БД, ES хранит серию неизменяемых событий.
Основные компоненты:
- Событие (Event): факт изменение состояния
- Агрегат (Aggregate): объект чье состояние хранится и восстанавливается из событий
- Event Store: БД оптимизированная для записи событий
- Проекция (Projection): представление агрегата (чтение), построенное из событий
Как это работает?
Изменение состояния:
- Система генерирует событие при любом изменении агрегата
- Событие сохраняется в Event Store
Восстановление состояния:
Чтобы получить текущее состояние агрегата, система последовательно применяет все его события
Создание проекций:
Чтобы каждый раз при чтении не перечитывать всю цепочку событий, их можно переложить в отдельную БД, в более удобный вид. Например в обычную таблицу. Здесь главное, что эту таблицу нельзя изменять, только читать из нее.
Преимущества и недостатки
Расскажу из опыта, мы начинали включать ES для сложных, агрегационных отчетах. Такой отчет очень легко пересчитывался и исправлялся в случае ошибки. Ведь вся история у нас есть, если есть ошибка в алгоритме расчета - просто исправляешь алгоритм и перечитываешь ивенты.
Следовательно преимущества:
- Полный аудит: есть вся история изменений.
- Простая отладка временных ошибок: можно воспроизвести состояние системы на любой момент.
- Очень гибко: легко создавать новые проекции.
При этом нужно было переосмыслить некоторые подходы к архитектуре, разобраться что такое CQRS и как с ним работать.
Я вполне представляю себе историю когда агрегат долго живет, у него очень много ивентов и его восстановление занимает какое-то время. Но это решается снэпшотом агрегата за какое-то стабильное время. Т.е. восстановили состояние агрегата и положили в реляционную БД. Далее при чтении сначала читаем снэпшот, а потом применяем к нему новые ивенты.
Где применять?
Там где важны исторические данные и точность - любые финансовые и медицинские системы.
Немного о технической реализации
Любое изменение агрегата должно генерировать ивент. Ивенты либо складываются во внутреннее поле агрегата, либо возвращаются из метода агрегата. Тут кому что нравится.
В данном случае пусть ивенты хранятся прям внутри агрегата.
Итого: использовать вообще везде ES я бы не стал, но есть некоторые контексты, где с ES жизнь была бы проще. Если уж решили использовать ES, то надо подумать о структуре ивентов, возможно версионировании ивента, миграциях.
#DDD
Это паттерн проектирования, при котором состояние агрегата определяется цепочкой событий. Т.е. вместо сохранения состояние в БД, ES хранит серию неизменяемых событий.
Основные компоненты:
- Событие (Event): факт изменение состояния
{ "type": "OrderCreated", "data": { "id": "123", "total": 100 } }- Агрегат (Aggregate): объект чье состояние хранится и восстанавливается из событий
- Event Store: БД оптимизированная для записи событий
- Проекция (Projection): представление агрегата (чтение), построенное из событий
Как это работает?
Изменение состояния:
- Система генерирует событие при любом изменении агрегата
- Событие сохраняется в Event Store
Восстановление состояния:
Чтобы получить текущее состояние агрегата, система последовательно применяет все его события
func RehydrateOrder(events []Event) *Order {
order := NewOrder()
for _, event := range events {
// Применяем событие к агрегату
order.Apply(event)
}
return order
}Создание проекций:
Чтобы каждый раз при чтении не перечитывать всю цепочку событий, их можно переложить в отдельную БД, в более удобный вид. Например в обычную таблицу. Здесь главное, что эту таблицу нельзя изменять, только читать из нее.
Преимущества и недостатки
Расскажу из опыта, мы начинали включать ES для сложных, агрегационных отчетах. Такой отчет очень легко пересчитывался и исправлялся в случае ошибки. Ведь вся история у нас есть, если есть ошибка в алгоритме расчета - просто исправляешь алгоритм и перечитываешь ивенты.
Следовательно преимущества:
- Полный аудит: есть вся история изменений.
- Простая отладка временных ошибок: можно воспроизвести состояние системы на любой момент.
- Очень гибко: легко создавать новые проекции.
При этом нужно было переосмыслить некоторые подходы к архитектуре, разобраться что такое CQRS и как с ним работать.
Я вполне представляю себе историю когда агрегат долго живет, у него очень много ивентов и его восстановление занимает какое-то время. Но это решается снэпшотом агрегата за какое-то стабильное время. Т.е. восстановили состояние агрегата и положили в реляционную БД. Далее при чтении сначала читаем снэпшот, а потом применяем к нему новые ивенты.
Где применять?
Там где важны исторические данные и точность - любые финансовые и медицинские системы.
Немного о технической реализации
Любое изменение агрегата должно генерировать ивент. Ивенты либо складываются во внутреннее поле агрегата, либо возвращаются из метода агрегата. Тут кому что нравится.
func (o *Order) Cancel() error {
if o.Status == "paid" {
return errors.New("невозможно отменить оплаченный заказ")
}
o.Status = "cancelled"
o.Version++
o.Events = append(o.Events, Event{
Type: "OrderCancelled",
Data: json.RawMessage(`{"reason": "client request"}`),
})
return nil
}В данном случае пусть ивенты хранятся прям внутри агрегата.
Итого: использовать вообще везде ES я бы не стал, но есть некоторые контексты, где с ES жизнь была бы проще. Если уж решили использовать ES, то надо подумать о структуре ивентов, возможно версионировании ивента, миграциях.
#DDD
Telegram
От сердца к коду
Тактические паттерны DDD: Aggregate
Ну вот мы и подошли к самому интересному. Кто-то считает что агрегаты это самое важное в технической части DDD. Кто-то их использует очень редко. Для меня это очень сложное понятие, давайте разбираться.
Если сущность…
Ну вот мы и подошли к самому интересному. Кто-то считает что агрегаты это самое важное в технической части DDD. Кто-то их использует очень редко. Для меня это очень сложное понятие, давайте разбираться.
Если сущность…
🫡3🔥2👍1
Делегирование
Перед любым руководителем, на любом из уровней, лежат какие-то задачи. Часть задач руководитель обязан решать сам, а часть задач нужно уметь делегировать. И умение правильно делегировать это очень важная тема.
Делегировать обязанность ради роста одного из разработчиков - очень полезно, а делегировать только рутину (скучные и низкоприоритетные задачи) - показать разработчику, что его работа не очень важна.
А что если не делегировать совсем? Скорее всего ты сам же и станешь узким местом, команда не будет расти, перестанет хватать времени и начнется выгорание. А еще, как тогда ходить в отпуск?
Для начала давайте разделим свои задачи на три категории:
1. Стратегические задачи. Например, архитектура. Оставляем себе, это наша зона ответственности.
2. Операционные задачи. Например, код-ревью или управление спринтами. Делегируем.
3. Рутина: автоматизируем.
Да, эти три категории относятся к руководителю выше техлида. Хотя, у техлида своя стратегия и операционка.
Ок, мы определились с операционными задачами, теперь надо решить кому их отдать? Было бы неплохо, если бы у нас был сотрудник, у которого сошлись бы сразу три следующих качества:
1. Скилл: хватает компетенций
2. Интерес: у сотрудника присутствует энтузиазм в отношении задачи.
3. Нагрузка: есть время и ресурсы.
Но конечно и из этого правила есть исключения, иногда можно намеренно делегировать задачу человеку с недостающим скиллом, но с высоким потенциалом. Тут просто надо понимать, что на первых порах от нас самих потребуется больше времени на поддержку.
Следующий этап: чётко поставить задачу:
- Цель: почему эта задача важна? Без почему - задача воспринимается как рутина.
- Ожидаемый результат: какой конечный результат вы хотите видеть?
- Сроки: если они есть.
- Ресурсы и полномочия: что есть, чтобы решить задачу. Какие решения сотрудник может принимать сам?
Дальше: не мешать! Не заниматься микроменеджментом! Это не значит, что надо полностью исчезнуть из процесса, быть всегда доступным для консультаций, но не вмешиваться без необходимости.
В одной из книг я прочитал, что делегирование, для лида, это не передача ответственности, а передача исполнения. Поэтому: контролируем, но не мешаем. Например, можно поставить еженедельные созвоны по проблеме, просто раз в день текстом пишет в чате о результатах, или договориться о демонстрации промежуточных результатов.
И последний шаг: после завершения задачи обязательно дать обратную связь сотруднику. Похвалить за успехи и указать что можно улучшить.
В целом я уверен в том, что любой руководитель должен всегда растить себе замену. Делегирование - это один из способов роста сотрудника. Кстати, я не говорил, что взятие дополнительных обязанностей на себя не должно быть отмечено материально. Если делегировали что-то действительно полезное для команды и важное, позаботьтесь и о финансовой мотивации также.
Выше, я написал что код-ревью это операционка. Для меня как для руководителя разработки - это так. А вот как быть с техлидом? Должен ли всегда только он делать код-ревью? Лично я считаю, что нет, часть ревью надо делегировать.
Итого, делегирование – это стратегический инструмент менеджера, а не просто сброс задач. Это инвестиция в рост команды и освобождение своего времени для того, чтобы заниматься стратегически важными задачами.
#management
Перед любым руководителем, на любом из уровней, лежат какие-то задачи. Часть задач руководитель обязан решать сам, а часть задач нужно уметь делегировать. И умение правильно делегировать это очень важная тема.
Делегировать обязанность ради роста одного из разработчиков - очень полезно, а делегировать только рутину (скучные и низкоприоритетные задачи) - показать разработчику, что его работа не очень важна.
А что если не делегировать совсем? Скорее всего ты сам же и станешь узким местом, команда не будет расти, перестанет хватать времени и начнется выгорание. А еще, как тогда ходить в отпуск?
Для начала давайте разделим свои задачи на три категории:
1. Стратегические задачи. Например, архитектура. Оставляем себе, это наша зона ответственности.
2. Операционные задачи. Например, код-ревью или управление спринтами. Делегируем.
3. Рутина: автоматизируем.
Да, эти три категории относятся к руководителю выше техлида. Хотя, у техлида своя стратегия и операционка.
Ок, мы определились с операционными задачами, теперь надо решить кому их отдать? Было бы неплохо, если бы у нас был сотрудник, у которого сошлись бы сразу три следующих качества:
1. Скилл: хватает компетенций
2. Интерес: у сотрудника присутствует энтузиазм в отношении задачи.
3. Нагрузка: есть время и ресурсы.
Но конечно и из этого правила есть исключения, иногда можно намеренно делегировать задачу человеку с недостающим скиллом, но с высоким потенциалом. Тут просто надо понимать, что на первых порах от нас самих потребуется больше времени на поддержку.
Следующий этап: чётко поставить задачу:
- Цель: почему эта задача важна? Без почему - задача воспринимается как рутина.
- Ожидаемый результат: какой конечный результат вы хотите видеть?
- Сроки: если они есть.
- Ресурсы и полномочия: что есть, чтобы решить задачу. Какие решения сотрудник может принимать сам?
Дальше: не мешать! Не заниматься микроменеджментом! Это не значит, что надо полностью исчезнуть из процесса, быть всегда доступным для консультаций, но не вмешиваться без необходимости.
В одной из книг я прочитал, что делегирование, для лида, это не передача ответственности, а передача исполнения. Поэтому: контролируем, но не мешаем. Например, можно поставить еженедельные созвоны по проблеме, просто раз в день текстом пишет в чате о результатах, или договориться о демонстрации промежуточных результатов.
И последний шаг: после завершения задачи обязательно дать обратную связь сотруднику. Похвалить за успехи и указать что можно улучшить.
В целом я уверен в том, что любой руководитель должен всегда растить себе замену. Делегирование - это один из способов роста сотрудника. Кстати, я не говорил, что взятие дополнительных обязанностей на себя не должно быть отмечено материально. Если делегировали что-то действительно полезное для команды и важное, позаботьтесь и о финансовой мотивации также.
Выше, я написал что код-ревью это операционка. Для меня как для руководителя разработки - это так. А вот как быть с техлидом? Должен ли всегда только он делать код-ревью? Лично я считаю, что нет, часть ревью надо делегировать.
Итого, делегирование – это стратегический инструмент менеджера, а не просто сброс задач. Это инвестиция в рост команды и освобождение своего времени для того, чтобы заниматься стратегически важными задачами.
#management
7💯5👍3🫡2