Добавить в корзинуПозвонить
Найти в Дзене

Как защитить бизнес-логику от мутаций в DTO: коротко

Сегодня рассмотрим как обезопасить бизнес-логику от случайного (или злонамеренного) изменения DTO, чем опасна мутабельность моделей и какие инструменты дают C#, Java, Python и Go, чтобы вы больше никогда не ловили эти баги. В одном слое DTO докрутили количество бонусных баллов и… нечаянно заменили Role. Ничего криминального, кроме того, что контракт authService ожидает неизменяемый объект. Получаем фейл авторизации и дырку в безопасности. Самый примитивный (и дорогостоящий) способ — копировать DTO при каждом входе/выходе слоя. UserDto safeCopy = incomingUserDto.clone(); // Допускается только чтение Минус: мусор в heap, забытые места, где копия не сделана. Используем AutoMapper/MapStruct/StructMapper, чтобы всегда создавать новые экземпляры. В .NET AutoMapper по умолчанию создаёт новый объект; добавляем PreserveReferences() только там, где действительно нужны циклы. В Java MapStruct генерирует код копирования на compile-time — лишний GC шум минимален. Сущность = данные + инварианты, но
Оглавление

Сегодня рассмотрим как обезопасить бизнес-логику от случайного (или злонамеренного) изменения DTO, чем опасна мутабельность моделей и какие инструменты дают C#, Java, Python и Go, чтобы вы больше никогда не ловили эти баги.

Классический затык: «невинный» UserDto

-2

В одном слое DTO докрутили количество бонусных баллов и… нечаянно заменили Role. Ничего криминального, кроме того, что контракт authService ожидает неизменяемый объект. Получаем фейл авторизации и дырку в безопасности.

Стратегия защиты

Защитные копии

Самый примитивный (и дорогостоящий) способ — копировать DTO при каждом входе/выходе слоя.

UserDto safeCopy = incomingUserDto.clone(); // Допускается только чтение

Минус: мусор в heap, забытые места, где копия не сделана.

Mapping-слой

Используем AutoMapper/MapStruct/StructMapper, чтобы всегда создавать новые экземпляры.

В .NET AutoMapper по умолчанию создаёт новый объект; добавляем PreserveReferences() только там, где действительно нужны циклы. В Java MapStruct генерирует код копирования на compile-time — лишний GC шум минимален.

Value Object-ы

Сущность = данные + инварианты, но без идентичности.

public readonly record struct Money(decimal Amount, string Currency);

У Value Object нет сеттеров, и его легче валидировать на входе.

Языковые инструменты анти-мутабельности

C# 12/13

-3

positional record по умолчанию immutable. А начиная с C# 12 к ним добавились required-члены и source-генератор init/required, позволяющий фиксировать состояние.

Java 21: record как контракт на неизменяемость

Java сравнительно поздно подошла к теме иммутабельных структур, но сделала это основательно. Ключевая конструкция — record. Когда вы пишете public record UserDto(String id, String role, String email) {}, компилятор генерирует private final поля, конструктор, equals, hashCode и toString.

Полезно в API-слоях, где важно, чтобы DTO, переданное наружу, оставалось нетронутым. Обновление таких объектов происходит только через создание новой версии: new UserDto(user.id(), "Admin", user.email()).

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

-4

До версии 2.13 Jackson не поддерживал record-ы, но начиная с 2.13 это работает корректно. На момент 2025 года предпочтительно использовать как минимум 2.17.

В Java рекомендует использовать record, когда объект не несёт поведения, а лишь передаёт данные.

Тем не менее, сами по себе record в качестве JPA-сущностей исподьзовать не стоит: Hibernate требует пустой конструктор и публичные сеттеры, чего у record-ов нет.

Python 3.13 + Pydantic v2: валидируем и замораживаем

В Pydantic v2 ключ к иммутабельности — параметр frozen=True в конфигурации модели.

Пример:

-5

Этот флаг делает все поля модели неизменяемыми: попытка изменения dto.role = 'admin' вызовет исключение. Модель становится hashable и может быть использована в set или в качестве ключа словаря.

С выходом Pydantic v2, построенного на Rust, производительность таких моделей выросла. В отличие от v1, где frozen работал непоследовательно, теперь это надёжная и быстрая конструкция.

Если использовать чистый Python, альтернатива — @dataclass(frozen=True). Пример:

-6

Имеем ту же иммутабельность, но без встроенной валидации. Это просто структурный контракт. Чтобы добавить проверки, нужны отдельные функции.

Для статического анализа можно использовать mypy с включённым плагином pydantic. Он поможет отлавливать попытки мутаций ещё на этапе разработки. В версиях mypy >= 1.10 появились базовые возможности отслеживания неизменности и для dataclass'ов, и для pydantic-моделей.

Вложенные модели также должны быть frozen, иначе вложенное состояние можно будет изменять. Об этом, к слову, часто забывают.

Go 1.22: значение по умолчанию — копия

В Go модель памяти устроена так, что передача структуры без указателя приводит к копированию. Это дает иммутабельность по дефолту. Рассмотрим структуру:

-7

В данном примере user это копия. Изменения внутри Promote не затрагивают оригинальный объект.

Проблемы начинаются, когда передаём указатель:

-8

В этом случае изменяем оригинальный объект. Поэтому в чистом сервис-слое рекомендуется использовать структуры по значению. Передача по указателю должна использоваться только там, где это оправдано: тяжёлые структуры, I/O операции, кэширование, необходимость синхронизации через sync.Mutex.

Для защиты от мутаций можно делать поля приватными и предоставлять только геттеры:

-9

Своего рода ручная иммутабельность. В бизнес-логике работа идёт только с геттер-методами, а изменить поля можно только через явно описанный билдер или фабрику.

В целом, Go поощряет явность: если вы передаёте указатель — значит, сознательно допускаете мутацию.

Мини-резюме

-10

Итоги

Immutable-подход — не панацея, но это дешевейшая страховка от пробелмы, которая рано или поздно возникает в микро- или макромонолитах. Чем раньше вы зацементируете DTO, тем меньше проблем будете решать потом.

Если вы отвечаете за развитие технической команды, то знаете, насколько важно вовремя закрывать дефицит навыков — без отрыва от работы и с фокусом на реальные задачи. В OTUS есть корпоративные программы именно под такие запросы: backend и frontend разработка, DevOps, аналитика, управление продуктами и процессами. Все курсы — практико-ориентированные, с возможностью адаптации под стек и цели вашей команды. Форматы — гибкие, чтобы обучение не мешало delivery.

Подробнее — на сайте OTUS.