DDD у Go. частина 1: Як FixDrive долає складність ride-hailing домену та уникає «великої купи бруду»


- Domain Layer: Сутності (Entities), агрегати та інтерфейси.
- Application Layer: Координує виконання сценаріїв (наприклад, процес оплати поїздки).
- Infrastructure Layer: Реалізація репозиторіїв, клієнти для Google Maps API.
- Presentation Layer: Обробка вхідних запитів (gRPC або HTTP)

- Entities (Сутності): Мають унікальний ID (наприклад, конкретна
Поїздка). Вони змінюються з часом, тому в Go ми завжди використовуємо вказівники для роботи з ними. - Value Objects (Об’єкти-значення): Не мають ID, порівнюються за значенням (наприклад,
Координати GPSабоВартість). Важливо: для них ми не використовуємо вказівники, щоб забезпечити їхню незмінність (immutability)



package domain
import (
"time"
)
// RideID — унікальний ідентифікатор поїздки
type RideID string
// RideStatus відображає життєвий цикл поїздки в ride-hailing сервісі
type RideStatus string
const (
RideStatusPending RideStatus = "pending" // Пошук водія
RideStatusAccepted RideStatus = "accepted" // Водій знайдений і прямує до клієнта
RideStatusInProgress RideStatus = "in_progress" // Пасажир у машині, їдуть
RideStatusCompleted RideStatus = "completed" // Поїздка успішно завершена
RideStatusCancelled RideStatus = "cancelled" // Скасовано пасажиром або водієм
)
// Ride — основна сутність для FixDrive.tech
type Ride struct {
ID RideID
PassengerID string // Хто замовляє
DriverID string // Хто виконує (може бути порожнім на етапі Pending)
// Маршрут
PickupPoint string // Точка А (адреса або координати)
Destination string // Точка Б
// Фінанси та сервіс
Price float64 // Вартість поїздки
Currency string // Наприклад, "UAH"
ServiceType string // Економ, Стандарт, Комфорт, Бізнес
Status RideStatus
CreatedAt time.Time // Час замовлення
StartedAt *time.Time // Час, коли пасажир сів у авто
FinishedAt *time.Time // Час завершення
}
Як це працює в архітектурі
Цей код належить до рівня Domain, тобто він описує правила бізнесу. Він не знає про базу даних чи API. Будь-яка інша частина програми (наприклад, сервіс розрахунку ціни або сервіс призначення водія) буде використовувати саме цю структуру як «єдине джерело істини»
package usecase
import (
"context"
"domain"
)
type RideUsecase struct {
rideRepo domain.RideRepository
passengerRepo domain.PassengerRepository
pricingService domain.PricingService // Специфічно для таксі
}
func (u *RideUsecase) CreateRideDraft(ctx context.Context, passengerID string, pickup, destination string) (*domain.Ride, error) {
// 1. Перевіряємо, чи існує пасажир та чи не заблокований він
passenger, err := u.passengerRepo.GetByID(ctx, passengerID)
if err != nil {
return nil, domain.ErrPassengerNotFound
}
// 2. Розраховуємо ціну (аналог вибору категорії/ціни в оголошеннях)
price, err := u.pricingService.Calculate(ctx, pickup, destination)
if err != nil {
return nil, err
}
// 3. Створюємо доменну сутність через конструктор (Business Logic)
ride, err := domain.NewRide(passenger.ID, pickup, destination, price)
if err != nil {
return nil, err
}
// 4. Зберігаємо чернетку в базу через репозиторій
if err := u.rideRepo.Save(ctx, ride); err != nil {
return nil, err
} return ride, nil
}
Репозиторій тепер — це просто «тупа» обгортка над базою даних. Він отримує готовий об’єкт Ride і кладе його в таблицю.

Валідація стану: Перед створенням поїздки ми можемо перевірити, чи немає у пасажира вже активної поїздки (бізнес-правило), що неможливо зробити всередині простого методу Save
- Pricing Service: У таксі ціна — залежить від відстані, заторів та попиту. Винесення логіки в Usecase дозволяє легко вставити сервіс прорахунку.
- Валідація стану: Перед створенням поїздки ми можемо перевірити, чи немає у пасажира вже активної поїздки (бізнес-правило), що неможливо зробити всередині простого методу
Інкрементальна розробка базується на ідеї поступового розширення функціональності системи. Продукт не створюється як монолітний блок — він «виростає» через окремі частини, кожна з яких додає нову цінність. Наприклад, у сервісі замовлення поїздок спочатку можна реалізувати базове створення заявки без складної логіки. Далі — додати розрахунок вартості, потім валідацію користувача, і вже після цього — статуси поїздки. Кожен такий крок є завершеним інкрементом: система стає функціональнішою, і її вже можна використовувати.
Ітеративна розробка, натомість, зосереджена не на розширенні, а на вдосконаленні. Та сама функція проходить кілька етапів розвитку: від простого прототипу до стабільного та оптимізованого рішення. Спочатку логіка може бути мінімальною, без глибокої обробки помилок чи оптимізації. З кожною ітерацією додаються перевірки, покращується структура коду, з’являється логування, метрики, підвищується продуктивність. У результаті функція залишається тією ж, але її якість суттєво зростає.
На практиці ці підходи майже ніколи не використовуються окремо. Сучасні команди поєднують їх. Наприклад, протягом одного спринту можна додати нову функціональність — це інкремент — і одночасно покращити вже існуючу частину системи — це ітерація. Такий підхід дозволяє балансувати між швидкою доставкою нових можливостей і підтримкою якості коду.
9 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівДякую за статтю! Не так багато матеріалів по DDD з прикладами на Go.
Чи поділяєте в коді обʼєкти явно на агрегати та сутності? Наприклад, чи може малознайомий з кодом інженер дивлячись на структуру одразу зрозуміти, що вона є саме агрегатом, а не сутністю?
Зі збереженням нового обʼєкта все доволі просто. Як зберігаєте зміни у вже існуючих? Особливо це цікаво в контексті роботи з агрегатами.
Хороше питання , Дмитре , я явно поділяю агрегати , я стараюсь найти корінь того агрегату ,в моєму коді яскравий приклад це «Поїздка» це ідеальний корінь.Бо ти не можеш змінити статус оплати або адресу призначення, не звернувшись до конкретної поїздки
Спасибо за статью!
А по какой причине Вы выбрали хранить Price в float64?
Если взять для примера Uklon или Bolt там цена без центов, какая была мотивация такого подхода?
Как решаете проблему округления?
Это статья про ДДД или на микроскопический разбор деталей?
Я к тому что из всей статьи для себя только нашли то что автор хранит в 64 битовом флоате.
Мне лично всё равно что там одно поле «не такое».
Мне идея, практические нюансы более важны.
При всем уважении, Вы попусту ищете на что придираться дабы автора время на пустые полемики тратить.
Если вам хочется людей обучать микро-оптимизациям.
Клава и комп есть, пишите свои статьи.
Уважаемый, тема ДДД никогда не было про скорость.
Нет, это не
статья, я поблагодарил автора за статью, вам возможно всё равно что там написано, Вы не поддержали комментарием, я вижу с того что Вы не оставили комментарий не поблагодарили автора, что тоже имеет смысл быть.
В статье прекрасные картинки, картинки в которых всё красиво показано, я бы мог спросить почему на картинки с слоями и описание слоев в разном порядке но я понимаю почему так может произойти. Автор не написал статью в вакууме что есть DDD и что я его вижу так, человек показал домен такси, и привёл вариант структуры, я считаю если автор что-то показывает то значит он желает чтобы люди на это обратили внимание или тогда зачем показывать структуру на 12 полей + комментарии? Если Вы видите эту статью в вакууме, она вам понравилась, то оставте комментарий, поделитесь впичетлениями.
Мой комментарий не был «а какого это ты х.ра float64 используешь», если вы не знаете проблему использовать float64 для цен, то почитайте это интересно. Тут не про оптимизацию то про прибыль которую можно потерять. Независимо ти думаешь доменами или таблицами в БД.
Уважаемый.
Пардон если я мыслю выдал как то агрессивно.
У меня не было цели в этом.
Проблему флотов знаю, на практике сталкивался при генерации налоговых отчетов для налоговой Германии, там потеря копейки в отчёте создаёт проблемы, потому мы решали хранением в целочисленном формате (заворачивая в money value object).
Насчёт поделится впечатлениями — Я не впечатлительный, чёрствый человек.
Насчёт поблагодарить — откуда инфа?) Я ему лично написал.
П.С. Трамп Зеленского тоже упрекал о неблагодарности 😂
Всех благ
По-перше я в жодному разі з міркувань NDA не виклав би комерційний код. По-друге я знайомий з концепцією Money Value Object , просто стаття не про це , а про використання DDD , це перша моя стаття та перша частина даної теми☺️. Погоджуюсь що доцільніше використовувати decimal , але як я вже і сказав стаття не про це. Я думаю я відповів на ваше запитання ☺️.
Дякую за статтю, чекаю продовження.
У своїх проєктах досі думаю категоріями «таблиць у БД», а в комерційних — мікс з DDD. Поки що жодного проєкту, де був би чистий DDD.
Дякую за статтю по DDD!
Борюсь зі складністю в проєктах, додаючи новий проєкт.