1. Давайте начнём не с терминов, а с запросов
Представьте самый обычный момент. У человека есть список для чтения, и он хочет добавить туда книгу Clean Code. Он не думает словами «сейчас я инициирую request». Он думает по-человечески: «хочу сохранить эту книгу себе». Но для backend-приложения любое такое желание должно превратиться в формальный запрос. Иначе системе пришлось бы угадывать смысл по интонации, а Java-приложения, к счастью, даром телепатии не обладают 🤯.
Пока нам не нужен настоящий HTTP. Достаточно увидеть саму форму запроса. Клиент отправляет в систему не «эмоцию», а структуру: что он хочет сделать и с какими данными. Даже в plain Java это уже можно показать очень просто:
// "Запрос" — то, что пришло на вход: минимальная полезная нагрузка для операции
record AddBookRequest(String query) {}
// "Ответ" — то, что вернём клиенту: статус + человекочитаемое сообщение
record AddBookResponse(int status, String message) {}
query здесь — ещё не финальная модель на все случаи жизни. Это просто учебная полезная нагрузка: строка, по которой мы ищем книгу. А status и message в ответе нужны не ради красоты. Они показывают важную вещь: система должна вернуть не просто «какой-то текст», а осмысленный результат — успешно или нет, и что именно произошло.
В этом месте backend и отличается от консольного стиля. В консольной программе вы могли бы просто напечатать Added и пойти дальше. В backend-сценарии важно помнить, что есть вторая сторона. Кто-то ждёт ответа и будет принимать решение по его форме. Это может быть браузер, мобильное приложение, Postman, другой сервис, а позже — и вы сами, когда начнёте отлаживать собственный API.
2. Участники маршрута🎢
Теперь разложим эту историю по ролям — не как список терминов из учебника, а как участников одного маршрута. Сначала есть клиент — тот, кто инициирует действие. Это может быть будущий UI, командная строка, Postman или другая программа. Клиент формирует запрос: «добавь книгу по этому поисковому запросу».
Дальше есть наше backend-приложение. Оно принимает вход, понимает, какую операцию нужно выполнить, и запускает прикладную обработку. Но на этом путь не заканчивается. Чтобы добавить книгу в список для чтения, нашему приложению удобно обратиться к внешнему каталогу книг. То есть мы не просто перекладываем локальные данные, а ещё и выступаем клиентом к другой системе. Это очень типичный backend-паттерн: ваш backend почти никогда не живёт в вакууме.
После внешнего поиска появляется ещё одна роль — собственное хранилище. Если книга найдена, её нужно запомнить в нашем ReadLater. Сегодня это пока лишь идея, позже будет in-memory реализация, а ещё позже вы увидите, почему такие вещи часто уезжают в базу данных. Но уже сейчас полезно различать «мы сходили наружу за данными» и «мы сохранили данные у себя».
И, наконец, у этого маршрута есть два разных результата. Первый — ответ клиенту: получилось добавить или нет. Второй — диагностический след: что происходило внутри. Именно поэтому позже мы будем так серьёзно относиться к логированию. Клиенту внутренние подробности не нужны, а разработчику как раз очень нужны.
3. Путь запроса целиком
Когда все участники названы, маршрут уже можно увидеть как цепочку. Пользователь или другой клиент инициирует действие. Наше приложение принимает запрос. Прикладная логика решает, что нужно сделать. Для этого она обращается к внешнему каталогу. Если книга найдена, она сохраняется в нашем списке чтения. Потом система формирует ответ и параллельно оставляет лог о том, как всё прошло.
Выглядит это примерно так:
sequenceDiagram
%% Участники "маршрута": кто с кем разговаривает
participant C as Клиент
participant B as ReadLater backend
participant S as Business logic
participant X as External catalog API
participant D as Reading list storage
%% Клиент инициирует действие (входной запрос)
C->>B: add book by query "clean code"
%% Backend передаёт управление в прикладную логику
B->>S: обработать вход
%% Прикладная логика ходит во внешнюю систему за данными
S->>X: найти книгу по query
%% Внешняя система возвращает либо данные, либо пустоту (негативный путь)
X-->>S: данные книги или пустой результат
%% Если нашли — сохраняем у себя (обновляем состояние)
S->>D: сохранить найденную книгу
D-->>S: ok
%% Возвращаем итог операции наверх по цепочке
S-->>B: итог операции
%% Backend формирует ответ клиенту
B-->>C: response
Эта картинка важна не потому, что нужно рисовать диаграммы на каждый чих 🙊. Она важна потому, что убирает магию. Ответ не «появляется» сам собой. Он вырастает из маршрута. И если где-то возникает проблема, искать её уже можно не по принципу «что-то сломалось», а по принципу «на каком участке цепочки это произошло» 🔍.
Ещё один важный момент: в backend удобно мыслить не файлами и не классами, а потоком действия. Кто пришёл? Что запросил? Где это разобрали? Какие правила применили? Куда сходили? Что сохранили? Что вернули? Чем точнее вы умеете проговорить этот путь, тем взрослее становится ваше инженерное мышление 🦥.
4. Небольшой Java-скелет
Чтобы маршрут не остался только стрелочками, соберём его в совсем маленькую Java-модель. Нам пока не нужны настоящие HTTP-вызовы, базы данных и JSON. Важнее увидеть роли и их границы.
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
// Доменная сущность: то, что в итоге хотим сохранить в ReadLater
record Book(String title, String author) {}
// Transport/контрактные модели: вход и выход операции
record AddBookRequest(String query) {}
record AddBookResponse(int status, String message) {}
// Порт (контракт) для внешнего каталога: backend зависит от интерфейса, а не от реализации
interface CatalogClient {
// Optional = "может быть найдено, а может и нет" (негативный сценарий — нормален)
Optional<Book> findFirstByTitle(String query);
}
// Заглушка внешнего каталога для обучения/тестов: всегда "находит" одну книгу
class StubCatalogClient implements CatalogClient {
@Override
public Optional<Book> findFirstByTitle(String query) {
// В реальности тут был бы HTTP-запрос во внешний API
return Optional.of(new Book("Clean Code", "Robert C. Martin"));
}
}
// Хранилище нашей системы: отдельная роль, отвечает только за сохранение
class ReadingListStorage {
// In-memory состояние: позже это место часто заменяется БД
private final List<Book> items = new ArrayList<>();
void save(Book book) {
// Минимальная реализация "persist"
items.add(book);
}
}
Здесь уже видно несколько здоровых привычек. Внешний каталог спрятан за интерфейсом CatalogClient, а не размазан по всему приложению. Хранилище играет отдельную роль и не смешивается с поиском книги. Код всё ещё очень маленький, но он уже не воспринимается как один большой main().
Теперь добавим прикладную логику и внешний ответ:
class ReadLaterService {
// Зависимость от внешнего каталога (интеграция)
private final CatalogClient catalogClient;
// Зависимость от собственного хранилища (состояние нашей системы)
private final ReadingListStorage storage;
ReadLaterService(CatalogClient catalogClient, ReadingListStorage storage) {
// Инъекция зависимостей: сервис не создаёт их сам
this.catalogClient = catalogClient;
this.storage = storage;
}
AddBookResponse addFirstMatch(AddBookRequest request) {
// 1) Ищем книгу во внешнем каталоге по query из запроса
return catalogClient.findFirstByTitle(request.query())
.map(book -> {
// 2) Happy-path: нашли книгу — сохраняем у себя
storage.save(book);
// 3) Формируем "контрактный" ответ клиенту
return new AddBookResponse(200, "Added: " + book.title());
})
.orElseGet(() ->
// Негативная ветка: ничего не нашли — возвращаем понятный статус/сообщение
new AddBookResponse(404, "Book not found")
);
}
}
Это не финальная архитектура и не попытка построить enterprise-замок на песке 🏗️. Но здесь уже читается backend-модель: вход пришёл, правило сработало, внешняя интеграция отработала, собственное состояние обновилось, наружу вернулся осмысленный ответ. Главное здесь — форма поведения системы, а не количество классов.
5. Где в этом маршруте живут ответ и логи 📝
Очень полезно с самого начала различать две вещи, которые в console-коде обычно смешаны. Ответ — это то, что должно уйти клиенту. Лог — это то, что помогает разработчику восстановить картину происходящего 🧭.
В примере выше AddBookResponse — это ответ. Он говорит клиенту: операция успешна или нет, и что именно произошло. Но если позже возникнет вопрос «почему книга не добавилась?» или «по какому запросу всё это вообще происходило?», ответа клиенту уже будет недостаточно. Нужен ещё внутренний след: что пришло на вход, что мы спросили у внешнего каталога, какой результат получили, сколько это заняло, была ли ошибка 🔎.
На старте курса мы ещё будем использовать println как учебный суррогат лога, и это нормально. Главное — уже сейчас не путать роли. Вот очень грубая иллюстрация:
class LoggingDemo {
AddBookResponse handle(AddBookRequest request) {
// Лог: внутренний диагностический след (не предназначен для клиента)
System.out.println("[log] query=" + request.query());
// Ответ: то, что уходит наружу как результат операции
return new AddBookResponse(200, "Added");
}
}
Смысл не в том, чтобы объявить println полноценной операционной стратегией. Смысл в том, чтобы глазами разделить два канала. Один нужен миру снаружи. Второй нужен нам. Когда эти каналы смешаны, всё начинает ломаться странным образом: клиент получает лишние внутренние детали, а разработчик не получает нормальной диагностики.
Именно отсюда позже вырастает важность SLF4J, Logback, лог-уровней и нормальных диагностических сообщений. Но фундамент здесь уже виден: backend — это наблюдаемая система, а не только вычислитель результата 👀.
6. Негативный путь — часть нормальной жизни 🧯
Самая опасная привычка новичка — думать, что сценарий заканчивается на happy-path. Нашёл книгу, сохранил, вернул 200, все счастливы 🎉. В учебных задачах такое видение ещё прощается. В backend — почти никогда.
Что может пойти не так даже в этом маленьком маршруте? Клиент может прислать пустой запрос. Внешний каталог может ничего не найти. Внешний каталог вообще может быть недоступен. Сохранение может завершиться не так, как ожидалось. И всё это не «редкие катастрофы», а нормальная часть жизни системы.
Поэтому взрослый backend-маршрут всегда включает негативные ветки. И это важный психологический сдвиг. Ошибка — не побочный шум вокруг «настоящего сценария». Ошибка — один из законных результатов работы системы. Позже вы увидите, как это превращается в HTTP-статусы, ответы с ошибками и контрактную дисциплину. Но зерно этой мысли должно лечь уже сейчас.
И вот здесь продуманный сценарий особенно полезен. Он показывает не абстрактную «архитектуру backend-а вообще», а конкретную причинно-следственную цепочку. Если вы понимаете, как в ReadLater проходит одно действие пользователя, то позже сможете спокойно привязать к этой цепочке и HTTP, и JSON, и Postman, и Java HTTP-клиент, и локальный API.
Следующий естественный вопрос звучит уже очень предметно: хорошо, маршрут понятен, но что вообще должно существовать в проекте, чтобы он был реальным, а не нарисованным на парте? Вот здесь и появляется скрытая ручная работа в backend-проекте — сборка, контракт, транспортные модели, конфигурация и логирование 🔧.
7. Типичные ошибки 🗿
Ошибка №1: воспринимать роли системы как абстрактные определения, а не как участников одного маршрута.
Если client, service, external API и storage существуют в голове отдельно, без живого действия, они быстро забываются. Как только вы привязываете их к одному сценарию — например, к добавлению книги в ReadLater — backend-картина становится гораздо устойчивее.
Ошибка №2: думать, что backend начинается только с настоящего HTTP-сервера.
HTTP важен, но модель появляется раньше. Даже plain Java-пример с Request, Response, сервисом и хранилищем уже учит мыслить входом, обработкой и результатом. Это и есть фундамент, на который позже аккуратно ляжет веб-слой.
Ошибка №3: смешивать ответ клиенту и диагностический вывод.
Когда println одновременно и «лог», и «пользовательский результат», система становится мутной. Клиенту нужен контрактный ответ, а разработчику — наблюдаемость. Эти две потребности пересекаются, но не совпадают.
Ошибка №4: считать негативный путь чем-то второстепенным.
Внешний API не обязан отвечать идеально, данные не обязаны быть чистыми, пользователь не обязан присылать прекрасный ввод. Если код написан только под happy-path, то это пока не backend-код, а демонстрация удачного случая.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ