- PVSM.RU - https://www.pvsm.ru -
Прежде чем погружаться в архитектуру, давайте посмотрим на контекст, в котором всё это происходит.
По данным исследования McKinsey 2022 года, технический долг составляет до 40% всего технологического портфеля компаний. И это не просто цифра в отчёте. Согласно опросу 2024 года среди технических руководителей, у более чем 50% компаний технический долг занимает свыше четверти всего IT-бюджета, блокируя внедрение новых функций. (Источник: vFunction, 2025 [1])
При этом исследование Carnegie Mellon выяснило, что наибольшим источником технического долга являются именно архитектурные проблемы — а не баги и не плохой код на уровне функций.
Теперь о Go. По данным Go Developer Survey 2024, главной проблемой команд, работающих с Go, названо поддержание единых стандартов кода — в том числе из-за разного уровня опыта участников и привнесения не-идиоматических паттернов из других языков. (Источник: go.dev/blog/survey2024-h2-results [2])
Это напрямую про нашу тему: люди приходят из Java, Python, C# и приносят с собой архитектурные привычки, которые в Go не работают. Clean Architecture и DDD — не исключение. Их часто реализуют "как в Java", а потом жалуются, что Go — многословный и неудобный язык.
Давайте разберёмся, как делать это правильно.
Как мы сюда попали?
Представьте: вы начинаете новый Go-сервис. Читаете статьи, смотрите видео, решаете "делать по-взрослому". Создаёте структуру:
internal/
domain/
application/
infrastructure/
delivery/
dto/
mappers/
interfaces/
services/
Через месяц у вас 200 файлов, пять слоёв абстракции и CreateOrderUseCase, который делает ровно одно: вызывает orderRepo.Save(). Бизнес-логики ноль. Зато интерфейсов — десять.
Знакомо? Это не Clean Architecture. Это тревожность, оформленная в папки.
Сегодня разберём, что такое DDD и Clean Architecture на самом деле, почему в Go их так часто делают неправильно, и как применять эти идеи прагматично — без оверинжиниринга.
Часть 1. Что вообще такое DDD?
Откуда всё взялось
Domain-Driven Design появился в 2003 году, когда Эрик Эванс написал книгу "Domain-Driven Design: Tackling Complexity in the Heart of Software" — Он работал с enterprise-системами и видел одну и ту же проблему: кодживёт в своём мире, а бизнес — в своём. Разработчики называют одно, менеджеры — другое, а потом все удивляются, почему система делает не то.
DDD — это моделирование предметной области. Набор практик для того, чтобы код говорил на языке бизнеса и отражал реальную предметную область.
Пять ключевых понятий DDD, которые вам нужны
1. Ubiquitous Language — единый язык
Суть: разработчики и эксперты предметной области должны использовать один и тот же язык. Не "мы говорим про entity, а они про клиента" — а одно слово для одного понятия везде: в разговорах, в документации, в коде.
Практически это означает: если менеджер говорит "подтвердить заказ" — в коде должен быть метод Confirm(), а не SetStatusConfirmed() или UpdateOrderState(). Если бухгалтер говорит "выставить счёт" — у вас должен быть Invoice, а не Bill или PaymentDocument.
Это кажется мелочью. Но когда новый разработчик читает код и понимает бизнес-логику без словаря — это и есть работающий Ubiquitous Language
2. Bounded Context — ограниченный контекст
Большие системы нельзя описать одной моделью. Понятие "клиент" в отделе продаж и в отделе поддержки — разные вещи. В продажах клиент — это лид с воронкой и статусами. В поддержке клиент — это тикет с историей обращений.
Bounded Context — это явная граница, внутри которой ваша модель последовательна и имеет смысл. За пределами этой границы та же сущность может быть другой — и это нормально.
В микросервисной архитектуре один сервис, как правило, и есть один Bounded Context. Но это не обязательно: один сервис может содержать несколько контекстов (если они слабо связаны), или один контекст может быть реализован несколькими сервисами.

3. Entity — сущность
Entity — объект, который имеет уникальную идентичность, сохраняющуюся во времени. Два объекта с одинаковыми атрибутами, но разными ID — это разные entity.
Order — entity. Даже если вы измените состав товаров или статус, это всё равно тот же самый заказ с тем же ID.
Важное свойство: entity содержит бизнес-логику, относящуюся к ней самой. Не просто данные, а данные плюс правила.
type Order struct {
id string
status OrderStatus
items []OrderItem
}
// Бизнес-правило живёт в entity, а не в сервисе
func (o *Order) Cancel() error {
if o.status == StatusShipped {
return errors.New("cannot cancel shipped order")
}
o.status = StatusCancelled
return nil
}
4. Value Object — объект-значение
Value Object — объект без идентичности. Два Value Object с одинаковыми атрибутами — одно и то же. Они неизменяемы: вы не меняете Value Object, вы создаёте новый.
type Money struct {
Amount int64 // в минимальных единицах: копейки, центы
Currency string
}
// Нет метода изменения — только создание нового
func (m Money) Add(other Money) (Money, error) {
if m.Currency != other.Currency {
return Money{}, errors.New("currency mismatch")
}
return Money{Amount: m.Amount + other.Amount, Currency: m.Currency}, nil
}
Другие примеры Value Object: адрес, координаты, диапазон дат, email, номер телефона. Всё, что определяется своими атрибутами, а не идентификатором.
5. Aggregate — агрегат
Агрегат — это кластер связанных объектов, которые обрабатываются как единица. У каждого агрегата есть Aggregate Root — корневой entity, через который происходит всё взаимодействие с кластером.
Order — Aggregate Root. OrderItem — часть агрегата. Вы никогда не меняете OrderItem напрямую — только через Order. Агрегат сам гарантирует свою консистентность.

Правило агрегатов: храните и загружайте агрегаты целиком. Транзакция должна затрагивать только один агрегат. Это — граница консистентности.
Часть 2. Clean Architecture — что это и зачем
История и идея
Clean Architecture предложил Роберт Мартин (Uncle Bob) в 2012 году, обобщив несколько похожих идей: Hexagonal Architecture Алистера Кокберна, Onion Architecture Джеффри Палермо и BCE-архитектуру Ивара Якобсона.
Все они об одном: бизнес-логика не должна зависеть от деталей реализации. База данных, HTTP-фреймворк, очереди сообщений — всё это детали. Детали меняются. Бизнес-логика должна оставаться стабильной.
Одно правило, которое важнее всего
В Clean Architecture есть Dependency Rule — правило зависимостей:
Зависимости в коде могут указывать только внутрь. Внутренние слои ничего не знают о внешних.

Что это означает на практике:
domain не импортирует ничего из вашего проекта
application знает только о domain
infrastructure знает о domain (через интерфейсы) и о внешних библиотеках
delivery знает об application, и иногда о domain для маппинга
Если у вас domain импортирует database/sql — вы нарушили правило. Если у вас HTTP-хендлер содержит SQL-запрос — вы тоже нарушили правило.
Зачем это нужно: три причины, а не абстрактная "чистота"
Тестируемость. Если доменная логика не зависит от базы данных — вы тестируете её без базы данных. Никаких test containers, никаких моков репозиториев для простых юнит-тестов. Просто вызываете метод и проверяете результат.
Замена деталей. Переехать с PostgreSQL на MongoDB или с REST на gRPC — это замена адаптера, а не переписывание бизнес-логики. В теории. На практике это работает именно тогда, когда вы честно соблюдали правило зависимостей.
Читаемость намерений. Когда бизнес-логика сосредоточена в домене, а не размазана по хендлерам и SQL-запросам — новый разработчик открывает domain/order.go и понимает, как работает заказ. Без погружения в детали инфраструктуры.
Clean Architecture & DDD
Это разные вещи, которые хорошо работают вместе.
|
|
DDD |
Clean Architecture |
|
Про что |
Моделирование предметной области |
Организация зависимостей |
|
Отвечает на вопрос |
Как описать бизнес в коде |
Как расположить слои и зависимости |
|
Главная идея |
Ubiquitous Language, Aggregates |
Dependency Rule |
|
Без чего работает |
Без конкретной структуры папок |
Без богатой доменной модели |
DDD даёт вам хорошую модель. Clean Architecture говорит, куда эту модель положить и как организовать зависимости вокруг неё.
Преимущества комбинации:
|
Аспект |
Эффект |
Метрика улучшения |
|
Тестируемость |
Изолированное тестирование домена |
+40% coverage |
|
Гибкость |
Замена адаптеров за часы |
-90% времени |
|
Понимание |
Чёткие границы компонентов |
-70% onboarding |
Часть 3. Как это выглядит в Go
Структура проекта
internal/
domain/
order.go ← агрегат, entity, value objects
order_repo.go ← интерфейс репозитория (порт)
errors.go ← доменные ошибки
application/
create_order.go ← use case
cancel_order.go
infrastructure/
postgres/
order_repo.go ← реализация порта (адаптер)
redis/
cache.go
delivery/
http/
order_handler.go
router.go
config/
config.go
cmd/
server/
main.go
Почему именно так:
domain — сердце. Здесь живёт всё, что описывает бизнес. Никаких сторонних импортов кроме стандартной библиотеки. application — оркестрация: собирает домен и вызывает его методы в нужном порядке. infrastructure — реализация всего, что имеет дело с внешним миром: базы данных, кеши, внешние API. delivery — точки входа: HTTP, gRPC, CLI, очереди.
Важно: интерфейс репозитория (order_repo.go) живёт в domain, а не в infrastructure. Именно это и реализует Dependency Rule — domain определяет, что ему нужно, а infrastructure реализует это. Не наоборот.
Полный пример: заказ в e-commerce
Разберём на конкретном примере, как это выглядит в живом Go-коде.
Domain: агрегат Order
// internal/domain/order.go
package domain
import (
"errors"
"time"
)
type OrderStatus string
const (
StatusPending OrderStatus = "pending"
StatusConfirmed OrderStatus = "confirmed"
StatusCancelled OrderStatus = "cancelled"
StatusShipped OrderStatus = "shipped"
)
// Money — Value Object. Нет ID, неизменяем, сравнивается по значению.
type Money struct {
Amount int64 // всегда в минимальных единицах (копейки, центы)
Currency string
}
func (m Money) Add(other Money) (Money, error) {
if m.Currency != other.Currency {
return Money{}, ErrCurrencyMismatch
}
return Money{Amount: m.Amount + other.Amount, Currency: m.Currency}, nil
}
// OrderItem — часть агрегата, не Entity (нет самостоятельной идентичности)
type OrderItem struct {
ProductID string
Name string
Qty int
UnitPrice Money
}
func (i OrderItem) Total() Money {
return Money{
Amount: i.UnitPrice.Amount * int64(i.Qty),
Currency: i.UnitPrice.Currency,
}
}
// Order — Aggregate Root
type Order struct {
id string
customerID string
items []OrderItem
status OrderStatus
createdAt time.Time
updatedAt time.Time
}
// NewOrder — фабричный метод, гарантирует создание валидного агрегата
func NewOrder(id, customerID string, items []OrderItem) (*Order, error) {
if id == "" {
return nil, ErrInvalidOrderID
}
if customerID == "" {
return nil, ErrInvalidCustomerID
}
if len(items) == 0 {
return nil, ErrEmptyOrder
}
for _, item := range items {
if item.Qty <= 0 {
return nil, ErrInvalidItemQty
}
}
now := time.Now()
return &Order{
id: id,
customerID: customerID,
items: items,
status: StatusPending,
createdAt: now,
updatedAt: now,
}, nil
}
// Confirm — бизнес-операция. Правила живут здесь, а не в сервисе.
func (o *Order) Confirm() error {
if o.status != StatusPending {
return ErrOrderAlreadyProcessed
}
o.status = StatusConfirmed
o.updatedAt = time.Now()
return nil
}
func (o *Order) Cancel() error {
if o.status == StatusShipped {
return ErrCannotCancelShipped
}
if o.status == StatusCancelled {
return ErrOrderAlreadyCancelled
}
o.status = StatusCancelled
o.updatedAt = time.Now()
return nil
}
func (o *Order) TotalAmount() Money {
if len(o.items) == 0 {
return Money{}
}
total := o.items[0].Total()
for _, item := range o.items[1:] {
var err error
total, err = total.Add(item.Total())
if err != nil {
// items с разными валютами не должны попасть в один заказ —
// это инвариант агрегата, гарантируем при создании
panic("invariant violation: mixed currencies in order")
}
}
return total
}
// Геттеры: поля приватные, доступ — только через методы
func (o *Order) ID() string { return o.id }
func (o *Order) CustomerID() string { return o.customerID }
func (o *Order) Status() OrderStatus { return o.status }
func (o *Order) Items() []OrderItem { return append([]OrderItem{}, o.items...) }
func (o *Order) CreatedAt() time.Time { return o.createdAt }
Domain: доменные ошибки
// internal/domain/errors.go
package domain
import "errors"
var (
ErrInvalidOrderID = errors.New("order id cannot be empty")
ErrInvalidCustomerID = errors.New("customer id cannot be empty")
ErrEmptyOrder = errors.New("order must have at least one item")
ErrInvalidItemQty = errors.New("item quantity must be positive")
ErrOrderAlreadyProcessed = errors.New("order is already processed")
ErrCannotCancelShipped = errors.New("cannot cancel shipped order")
ErrOrderAlreadyCancelled = errors.New("order is already cancelled")
ErrCurrencyMismatch = errors.New("currency mismatch")
)
Domain: порт репозитория
// internal/domain/order_repo.go
package domain
import "context"
// OrderRepository — это порт (интерфейс в терминах Hexagonal Architecture).
// Определяем здесь, в домене. Реализуем — в infrastructure.
type OrderRepository interface {
Save(ctx context.Context, order *Order) error
FindByID(ctx context.Context, id string) (*Order, error)
FindByCustomerID(ctx context.Context, customerID string) ([]*Order, error)
}
Application: Use Case
// internal/application/create_order.go
package application
import (
"context"
"fmt"
"github.com/google/uuid"
"yourapp/internal/domain"
)
type CreateOrderInput struct {
CustomerID string
Items []domain.OrderItem
}
type CreateOrderOutput struct {
OrderID string
TotalAmount domain.Money
}
type CreateOrderUseCase struct {
orders domain.OrderRepository
// сюда можно добавить: eventPublisher, notifier, inventoryChecker — без страха
}
func NewCreateOrderUseCase(orders domain.OrderRepository) *CreateOrderUseCase {
return &CreateOrderUseCase{orders: orders}
}
func (uc *CreateOrderUseCase) Execute(ctx context.Context, in CreateOrderInput) (CreateOrderOutput, error) {
id := uuid.New().String()
order, err := domain.NewOrder(id, in.CustomerID, in.Items)
if err != nil {
return CreateOrderOutput{}, fmt.Errorf("create order: %w", err)
}
if err := order.Confirm(); err != nil {
return CreateOrderOutput{}, fmt.Errorf("confirm order: %w", err)
}
if err := uc.orders.Save(ctx, order); err != nil {
return CreateOrderOutput{}, fmt.Errorf("save order: %w", err)
}
return CreateOrderOutput{
OrderID: order.ID(),
TotalAmount: order.TotalAmount(),
}, nil
}
Infrastructure: адаптер
// internal/infrastructure/postgres/order_repo.go
package postgres
import (
"context"
"database/sql"
"fmt"
"yourapp/internal/domain"
)
type OrderRepository struct {
db *sql.DB
}
func NewOrderRepository(db *sql.DB) *OrderRepository {
return &OrderRepository{db: db}
}
func (r *OrderRepository) Save(ctx context.Context, order *domain.Order) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx,
`INSERT INTO orders (id, customer_id, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO UPDATE
SET status = $3, updated_at = $5`,
order.ID(), order.CustomerID(), string(order.Status()),
order.CreatedAt(), order.UpdatedAt(),
)
if err != nil {
return fmt.Errorf("upsert order: %w", err)
}
// здесь же сохраняем items — агрегат сохраняется целиком
for _, item := range order.Items() {
_, err = tx.ExecContext(ctx,
`INSERT INTO order_items (order_id, product_id, qty, unit_price, currency)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (order_id, product_id) DO NOTHING`,
order.ID(), item.ProductID, item.Qty,
item.UnitPrice.Amount, item.UnitPrice.Currency,
)
if err != nil {
return fmt.Errorf("insert order item: %w", err)
}
}
return tx.Commit()
}
func (r *OrderRepository) FindByID(ctx context.Context, id string) (*domain.Order, error) {
// маппинг из SQL → domain.Order
// используем приватный конструктор или builder для восстановления агрегата
// ...
return nil, nil
}
func (r *OrderRepository) FindByCustomerID(ctx context.Context, customerID string) ([]*domain.Order, error) {
// ...
return nil, nil
}
Delivery: HTTP-хендлер
// internal/delivery/http/order_handler.go
package httpdelivery
import (
"encoding/json"
"errors"
"net/http"
"yourapp/internal/application"
"yourapp/internal/domain"
)
type OrderHandler struct {
createOrder *application.CreateOrderUseCase
}
func NewOrderHandler(createOrder *application.CreateOrderUseCase) *OrderHandler {
return &OrderHandler{createOrder: createOrder}
}
type createOrderRequest struct {
CustomerID string `json:"customer_id"`
Items []struct {
ProductID string `json:"product_id"`
Name string `json:"name"`
Qty int `json:"qty"`
PriceCents int64 `json:"price_cents"`
Currency string `json:"currency"`
} `json:"items"`
}
type createOrderResponse struct {
OrderID string `json:"order_id"`
TotalAmountCents int64 `json:"total_amount_cents"`
Currency string `json:"currency"`
}
func (h *OrderHandler) Create(w http.ResponseWriter, r *http.Request) {
var req createOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
items := make([]domain.OrderItem, len(req.Items))
for i, it := range req.Items {
items[i] = domain.OrderItem{
ProductID: it.ProductID,
Name: it.Name,
Qty: it.Qty,
UnitPrice: domain.Money{Amount: it.PriceCents, Currency: it.Currency},
}
}
out, err := h.createOrder.Execute(r.Context(), application.CreateOrderInput{
CustomerID: req.CustomerID,
Items: items,
})
if err != nil {
// маппинг доменных ошибок в HTTP-статусы
switch {
case errors.Is(err, domain.ErrEmptyOrder),
errors.Is(err, domain.ErrInvalidItemQty):
http.Error(w, err.Error(), http.StatusBadRequest)
default:
http.Error(w, "internal error", http.StatusInternalServerError)
}
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(createOrderResponse{
OrderID: out.OrderID,
TotalAmountCents: out.TotalAmount.Amount,
Currency: out.TotalAmount.Currency,
})
}
Сборка в main.go — никакой магии
// cmd/server/main.go
package main
import (
"database/sql"
"log"
"net/http"
_ "github.com/lib/pq"
"yourapp/internal/application"
httpdelivery "yourapp/internal/delivery/http"
"yourapp/internal/infrastructure/postgres"
)
func main() {
db, err := sql.Open("postgres", "postgres://...")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Сборка: каждая зависимость явная
orderRepo := postgres.NewOrderRepository(db)
createOrderUC := application.NewCreateOrderUseCase(orderRepo)
orderHandler := httpdelivery.NewOrderHandler(createOrderUC)
mux := http.NewServeMux()
mux.HandleFunc("POST /orders", orderHandler.Create)
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}

Часть 4. Антипаттерны — что делают неправильно
Интерфейсы "на всякий случай"
// Это бессмысленно, если реализация одна
type OrderServiceInterface interface {
Create(dto CreateOrderDTO) error
Update(dto UpdateOrderDTO) error
Delete(id string) error
}
type OrderService struct{}
func (s *OrderService) Create(dto CreateOrderDTO) error { ... }
В Go интерфейсы — неявные. Их нужно определять там, где они потребляются, а не там, где они реализуются. Если у вашего сервиса одна реализация и нет планов делать вторую — интерфейс не нужен.
// Правильно: если нужно тестировать UseCase без реальной БД,
// интерфейс определяется рядом с UseCase, в домене
type OrderRepository interface {
Save(ctx context.Context, order *Order) error
}
// А не рядом с Postgres-реализацией
Анемичная доменная модель
// Плохо: Order — просто мешок с данными
type Order struct {
ID string
Status string
Items []Item
}
// Логика вынесена в сервис — это антипаттерн DDD
func (s *OrderService) Confirm(order *Order) error {
if len(order.Items) == 0 {
return errors.New("empty")
}
order.Status = "confirmed"
return nil
}
Когда вся логика в сервисах, а объекты — это только данные, вы получаете процедурный код с красивыми названиями классов. Анемичная модель — главный враг DDD.
// Хорошо: логика — часть объекта
func (o *Order) Confirm() error {
if len(o.items) == 0 {
return domain.ErrEmptyOrder
}
if o.status != StatusPending {
return domain.ErrOrderAlreadyProcessed
}
o.status = StatusConfirmed
return nil
}
Бизнес-логика в хендлере
// Плохо: хендлер принимает бизнес-решения
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
var req Request
json.NewDecoder(r.Body).Decode(&req)
// Это не должно быть здесь
if len(req.Items) == 0 {
http.Error(w, "empty order", 400)
return
}
if req.TotalAmount > 100_000_00 {
http.Error(w, "amount too large", 400)
return
}
if req.CustomerID == "banned_customer" {
http.Error(w, "forbidden", 403)
return
}
// ...
}
Хендлер должен заниматься только двумя вещами: извлекать данные из HTTP-запроса и отдавать HTTP-ответ. Всё остальное — в домен или в UseCase.
"Четыре слоя в CRUD-сервисе"
Если у вашего сервиса три эндпоинта и вся "логика" — это SELECT * FROM users WHERE id = $1, то GetUserUseCase с торжественным именем — это церемония ради церемонии.
// Для простого CRUD это нормально
func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
user, err := h.repo.FindByID(r.Context(), id)
if err != nil {
http.Error(w, "not found", 404)
return
}
json.NewEncoder(w).Encode(user)
}
Три строки, один файл. Никакого UseCase-слоя. Добавите его, когда появится реальная логика.
Часть 5. Где упростить — и не надо стесняться
Есть три ситуации, когда полная Clean Architecture избыточна.
Ситуация 1: Нет бизнес-логики. Если ваш сервис — это прокси к базе данных (создать / получить / обновить / удалить без правил), UseCase-слой ничего не даёт. Хендлер → репозиторий. Добавите оркестрацию, когда она появится.
Ситуация 2: Маленький сервис. До 10 эндпоинтов и 5 доменных объектов — слои добавляют больше сложности, чем снимают. Начните с плоской структуры и рефакторьте по мере роста.
Ситуация 3: MVP и прототипы. Скорость важнее структуры. Сначала докажите, что идея работает, потом приводите в порядок архитектуру.
Главное правило: начинайте с домена, а не со слоёв. Сначала ответьте на вопрос "Что такое Order? Какие у него правила?" Потом думайте о слоях.
Вывод
DDD и Clean Architecture решают разные проблемы и хорошо работают вместе — но только если вы понимаете, зачем они нужны.
DDD — это про смысл. Ваш код должен говорить на языке бизнеса. Если читая Order.Confirm() вы понимаете, что происходит в бизнесе — DDD работает. Если читая OrderService.SetStatusConfirmed() вы не понимаете, при каких условиях это вызывается — DDD нет.
Clean Architecture — это про зависимости. Одно правило: внешнее зависит от внутреннего, не наоборот. База данных знает про домен. Домен не знает про базу данных. Это и есть суть.
Go — это про простоту. Go не любит неявную магию, не любит глубокие иерархии классов, не любит интерфейсы ради интерфейсов. Хорошая Go-архитектура — это та, где зависимости явные, модули маленькие, а код читается сверху вниз без прыжков по десяти файлам.
Начните с хорошей доменной модели. Добавляйте слои только когда они действительно требуются. И помните: цель архитектуры — не красивая структура папок, а код, который легко читать, легко тестировать и легко менять.
Автор: gugkaev
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/patterny-proektirovaniya/449919
Ссылки в тексте:
[1] vFunction, 2025: https://vfunction.com/blog/how-to-manage-technical-debt/
[2] go.dev/blog/survey2024-h2-results: https://go.dev/blog/survey2024-h2-results
[3] Источник: https://habr.com/ru/articles/1025068/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1025068
Нажмите здесь для печати.