Изоляция модулей - подробно о module-isolation на практических примерах

05 января 2026
Автор

Олег Марков

Введение

Изоляция модулей (module-isolation) — это подход к организации кода, при котором каждая часть системы имеет четкие границы, минимально раскрывает свою внутреннюю реализацию и взаимодействует с остальными компонентами только через формальные интерфейсы.

Смотрите, я покажу вам, как изоляция модулей помогает:

  • снизить связность между компонентами;
  • упростить тестирование и отладку;
  • повысить безопасность (минимизировать доступ к внутренним данным и состояниям);
  • облегчить рефакторинг и эволюцию архитектуры.

В этой статье мы будем говорить не о конкретном фреймворке, а о практической технике module-isolation, которую можно применять в разных языках и платформах — от JavaScript и TypeScript до Go, Java, .NET или Python. Примеры я буду давать в нескольких языках, чтобы вы могли перенести подход в свои проекты.

Основные принципы изоляции модулей

Что такое модуль в контексте module-isolation

Под модулем будем понимать относительно автономную единицу системы, которая:

  • реализует определенную ответственность (например, "работа с платежами", "аутентификация", "репозиторий пользователей");
  • скрывает детали реализации (структуры данных, конкретные алгоритмы, кеши, логику валидации);
  • предоставляет внешний контракт — интерфейс, публичные функции, классы, REST API, события.

Важно: изоляция модулей — это не только про физическую упаковку (файлы, пакеты, библиотеки), но и про логические границы:

  • что считается публичным;
  • что должно всегда оставаться приватным;
  • какие зависимости допустимы между модулями.

Ключевые цели изоляции

Давайте разберем, чего именно вы добиваетесь, внедряя module-isolation:

  1. Инкапсуляция данных и логики

    Внешний код не должен напрямую управлять внутренними структурами модуля. Это снижает риск поломать инварианты и вводить неявные зависимости.

  2. Снижение связности (coupling)

    Если модуль A знает слишком много о модуле B, любое изменение в B приведет к изменению A. Изоляция уменьшает такие связи: модули общаются через стабильные интерфейсы.

  3. Простота подмены реализации

    Когда у вас есть четкий интерфейс модуля, становится проще:

    • подменить реализацию на тестовую;
    • сделать альтернативную версию (например, in-memory и version с базой данных);
    • постепенно переписать модуль, не ломая клиентов.
  4. Безопасность и контроль доступа

    Изоляция помогает ограничить:

    • какие функции доступны другим модулям;
    • кто может менять состояние;
    • какие данные можно передавать наружу.
  5. Масштабирование разработки

    Когда над проектом работает много людей и команд, module-isolation позволяет им работать относительно независимо, минимизируя конфликтующие изменения.

Типы границ при изоляции модулей

Вы можете работать с несколькими типами границ одновременно:

  • Синтаксические границы
    Это то, что поддерживает сам язык: private, public, internal, пакеты, сборки, модули.
    Пример: в Go то, что начинается с маленькой буквы, не экспортируется за пределы пакета.

  • Логические границы
    Даже если язык не предоставляет жесткой модульности, вы можете договориться, что, например, "все обращения к БД идут только через модуль repository". Это архитектурная договоренность, подкрепленная код-ревью и линтерами.

  • Процессные и сетевые границы
    Модуль может быть вынесен в отдельный сервис (микросервис), контейнер, процесс. Тогда изоляция реализуется через сетевые протоколы и права доступа.

В этой статье мы сфокусируемся на модульной изоляции внутри одного приложения, но многие принципы легко переносятся и на микросервисную архитектуру.

Организация структуры модулей в проекте

Пример структуры на уровне директорий

Посмотрим на пример условного проекта на TypeScript с разделением по модулям:

// Структура проекта
src/
  user/
    index.ts        // Публичный интерфейс модуля user
    user.service.ts // Внутренняя логика работы с пользователями
    user.model.ts   // Модели и типы, не экспортируемые наружу
    user.repo.ts    // Работа с хранилищем данных для пользователей
  billing/
    index.ts
    billing.service.ts
    billing.gateway.ts
  shared/
    logger.ts       // Общий логгер
    types.ts        // Общие типы

Здесь модуль user изолирован:

  • наружу экспортируются только сущности из src/user/index.ts;
  • остальные файлы рассматриваются как детали реализации.

Теперь вы увидите, как это выглядит в коде.

// src/user/index.ts
// Это публичное "лицо" модуля user

// Экспортируем только то, что нужно внешним модулям
export { createUser, getUserById } from "./user.service";
export type { PublicUserDTO } from "./user.model";
// src/user/user.service.ts
// Внутренняя логика работы с пользователями

import { saveUser, findUserById } from "./user.repo";
import { InternalUser, PublicUserDTO } from "./user.model";

export async function createUser(email: string): Promise<PublicUserDTO> {
  // Здесь мы создаем внутреннюю модель пользователя
  const user: InternalUser = {
    id: generateId(), // Вспомогательная функция, не экспортируемая наружу
    email,
    createdAt: new Date(),
  };

  await saveUser(user);

  // Конвертируем внутреннюю модель в DTO, доступное внешнему миру
  return {
    id: user.id,
    email: user.email,
  };
}

export async function getUserById(id: string): Promise<PublicUserDTO | null> {
  const user = await findUserById(id);
  if (!user) return null;

  return {
    id: user.id,
    email: user.email,
  };
}

// Функция, которая не экспортируется и видна только внутри файла
function generateId(): string {
  return `user_${Math.random().toString(36).slice(2)}`;
}
// src/user/user.model.ts
// Разделяем внутренние и внешние типы пользователя

// Внутренняя модель, недоступная снаружи (не экспортируется в index.ts)
export interface InternalUser {
  id: string;
  email: string;
  createdAt: Date;
}

// DTO, доступное внешнему миру
export interface PublicUserDTO {
  id: string;
  email: string;
}

Обратите внимание:

  • другой модуль может импортировать только то, что идет из user/index.ts;
  • внутренние структуры (InternalUser, generateId) не "просачиваются" наружу;
  • это и есть базовая изоляция модуля.

Слои внутри модуля

Изоляция модулей хорошо сочетается с разделением на слои внутри самого модуля:

  • уровень API модуля (файл index.ts, публичные функции, интерфейсы);
  • уровень бизнес-логики (службы, сервисы);
  • уровень доступа к данным (репозитории, клиенты внешних сервисов).

Например, в модуле user:

  • публичный слой — функции createUser и getUserById;
  • логика — в user.service.ts;
  • данные — в user.repo.ts.

Так вы избегаете ситуации, когда другие модули напрямую лезут в БД модуля user. Вместо этого они пользуются его контрактом.

Работа с экспортами и видимостью

Экспортируем только то, что действительно нужно

Даже если язык по умолчанию делает сущности "видимыми" (например, JavaScript без модулей), стоит сознательно ограничивать публичные точки входа.

Давайте разберемся на примере Go, где экспорт определяется с помощью заглавной буквы.

// user/repository.go
package user

// internalUser - неэкспортируемый тип, доступен только внутри пакета user
type internalUser struct {
    id    string
    email string
}

// UserDTO - экспортируемый тип, публичная "проекция" пользователя
type UserDTO struct {
    ID    string
    Email string
}

// findUserByID - неэкспортируемая функция
func findUserByID(id string) (*internalUser, error) {
    // Здесь мог бы быть запрос к БД
    return &internalUser{
        id:    id,
        email: "test@example.com",
    }, nil
}

// GetUser - экспортируемая функция, публичная точка входа в модуль
func GetUser(id string) (*UserDTO, error) {
    // Используем внутреннюю функцию для получения внутренних данных
    u, err := findUserByID(id)
    if err != nil {
        return nil, err
    }

    // Преобразуем внутреннюю модель к DTO
    return &UserDTO{ID: u.id, Email: u.email}, nil
}

Как видите:

  • внутренний тип internalUser и функция findUserByID изолированы внутри пакета;
  • наружу вы отдаете только UserDTO и GetUser.

Такой подход позволяет вам позже переписать работу с БД, не затрагивая код, который использует модуль user.

Использование "фасада" модуля

Файл-фасад (gateway, index, api) — это точка, через которую другие части системы обращаются к модулю. Это удобный способ:

  • ограничить, какие функции и классы считаются официальными;
  • скрыть внутреннюю структуру файлов модуля.

Например, в Java:

// user/UserModule.java
// Фасад модуля User

package user;

public class UserModule {

    // Внутренняя зависимость, скрытая от внешнего кода
    private final UserRepository repository = new UserRepository();

    // Публичный метод - официальный контракт модуля
    public UserDTO getUser(String id) {
        // Вызываем скрытый репозиторий
        UserEntity entity = repository.findById(id);
        if (entity == null) {
            return null;
        }

        // Преобразуем сущность к DTO
        UserDTO dto = new UserDTO();
        dto.id = entity.id;
        dto.email = entity.email;
        return dto;
    }
}

Внешний код работает только с UserModule и UserDTO, а детали вроде UserRepository и UserEntity остаются внутри модуля.

Правило "минимально достаточного" интерфейса

При проектировании интерфейса модуля полезно придерживаться правила:
экспортируйте только то, без чего действительно невозможно использовать модуль по назначению.

Что это дает:

  • меньше точек связности;
  • проще рефакторинг интерфейса;
  • меньше риск случайно "закрепить" во внешнем контракте детали реализации.

Управление зависимостями между модулями

Направление зависимостей

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

  • "бизнесовые" модули не должны зависеть от "инфраструктурных деталей" (например, конкретной СУБД);
  • желательно избегать циклических зависимостей между модулями.

Пример: у вас есть модуль user и модуль billing.
Как лучше организовать зависимости?

  • Вариант 1: billing зависит от user — нормально, если биллинг проверяет состояние пользователя.
  • Вариант 2: user зависит от billing — рискованно, потому что пользовательский профиль начинает знать о платежах.

Чаще всего выбирают направление от более обобщенной бизнес-логики к более специализированной.

Общие модули и "ядро" системы

Обычно в проекте появляются:

  • модули-ядро (core, domain, shared-kernel) — с ключевой бизнес-логикой;
  • инфраструктурные модули (db, logger, http, mq);
  • прикладные модули (user, billing, auth, notifications).

Хорошая практика:
"ядро" не должно зависеть от инфраструктурных деталей. Вместо этого оно описывает контракты, а конкретные реализации подставляются снаружи.

Посмотрим на примере интерфейса репозитория в TypeScript.

// core/user/user.port.ts
// Здесь мы описываем порт (контракт) репозитория пользователей

export interface UserRepository {
  findById(id: string): Promise<User | null>;
  save(user: User): Promise<void>;
}

// Внутренняя бизнес-модель пользователя
export interface User {
  id: string;
  email: string;
}
// infrastructure/user/user.repo.mongo.ts
// Конкретная реализация репозитория, завязанная на MongoDB

import { UserRepository, User } from "../../core/user/user.port";

export class MongoUserRepository implements UserRepository {
  // В конструктор можно передать клиент MongoDB
  constructor(private readonly client: any) {}

  async findById(id: string): Promise<User | null> {
    // Здесь запрос к MongoDB
    const doc = await this.client.collection("users").findOne({ id });
    if (!doc) return null;

    // Маппинг документа на доменную модель
    return {
      id: doc.id,
      email: doc.email,
    };
  }

  async save(user: User): Promise<void> {
    await this.client.collection("users").updateOne(
      { id: user.id },
      { $set: user },
      { upsert: true }
    );
  }
}
// app/bootstrap.ts
// Зависимость между модулем ядра и инфраструктурой связывается здесь

import { MongoClient } from "mongodb";
import { MongoUserRepository } from "../infrastructure/user/user.repo.mongo";
import { createUserService } from "../core/user/user.service";

async function bootstrap() {
  // Инициализируем клиент MongoDB
  const client = await MongoClient.connect("mongodb://localhost:27017/app");

  const userRepo = new MongoUserRepository(client);
  // Передаем репозиторий в доменный сервис
  const userService = createUserService(userRepo);

  // Теперь userService можно использовать в HTTP-контроллерах и т.д.
}

Здесь модуль "ядра" (core/user) не знает ни о MongoDB, ни о конкретных клиентах. Это усиливает изоляцию: домен не зависит от инфраструктуры.

Соглашения о зависимостях и линтеры

Чтобы поддерживать module-isolation в живом проекте, полезно:

  • ввести правила зависимостей (например, модуль core не может зависеть от infrastructure);
  • использовать линтеры или кастомные скрипты для проверки импортов.

Пример простого правила:
"Файлы из core/ могут импортировать только из core/ и shared/, но не из infrastructure/".

В экосистеме JavaScript/TypeScript для этого часто используют:

  • eslint с правилами по паттернам импортов;
  • инструменты анализа зависимостей (madge, dependency-cruiser).

Изоляция состояния и побочных эффектов

Почему состояние важно изолировать

Даже если вы разделили код на файлы и модули, они могут быть логически связаны через:

  • глобальные переменные;
  • синглтоны;
  • общие кеши;
  • общие конфигурации.

Это снижает модульную изоляцию: изменение состояния в одном месте может неожиданно повлиять на другие.

Лучше:

  • сделать состояние частью модуля;
  • предоставлять строго ограниченные операции для изменения состояния;
  • по возможности делать модули чистыми, без скрытых побочных эффектов.

Пример изоляции состояния в модуле

Давайте посмотрим на пример на JavaScript/TypeScript, который организует "хранилище" с внутренним состоянием.

// counter/module.ts
// Модуль со скрытым состоянием счетчика

// Внутреннее состояние модуля, не экспортируется наружу
let value = 0;

export function increment(): void {
  // Увеличиваем внутреннее состояние
  value += 1;
}

export function getValue(): number {
  // Читаем значение, не давая доступ напрямую к переменной value
  return value;
}

export function reset(): void {
  // Сбрасываем состояние счетчика
  value = 0;
}

Другие модули могут увеличивать и читать счетчик, но не могут напрямую изменить value чем-то неожиданным. Это простейший пример изоляции состояния.

Если вам нужно несколько независимых экземпляров, стоит вынести состояние в объект:

// counter/factory.ts
// Фабрика для создания независимых счетчиков

export interface Counter {
  increment(): void;
  getValue(): number;
  reset(): void;
}

export function createCounter(): Counter {
  // Состояние инкапсулировано внутри фабрики
  let value = 0;

  return {
    increment() {
      value += 1;
    },
    getValue() {
      return value;
    },
    reset() {
      value = 0;
    },
  };
}

Теперь любой клиент модуля может создать свой независимый экземпляр счетчика, и состояния между ними не перемешиваются.

Изоляция побочных эффектов

Побочные эффекты — это операции вроде:

  • запись в файл;
  • обращение к сети;
  • изменение глобальных объектов;
  • логирование.

Хорошая практика в module-isolation:

  • выносить побочные эффекты на "края" модуля;
  • внутренняя логика по возможности остаётся чистой (pure).

Например, при валидации данных:

// user/validation.ts
// Чистая функция валидации, без логов и запросов к сети

export interface ValidationResult {
  isValid: boolean;
  errors: string[];
}

export function validateEmail(email: string): ValidationResult {
  const errors: string[] = [];

  if (!email.includes("@")) {
    errors.push("Email должен содержать символ @");
  }

  if (email.length < 5) {
    errors.push("Email слишком короткий");
  }

  return {
    isValid: errors.length === 0,
    errors,
  };
}
// user/service.ts
// Сервис, который вызывает валидацию и логирует результат

import { validateEmail } from "./validation";
import { logger } from "../shared/logger";

export function registerUser(email: string) {
  const result = validateEmail(email);

  if (!result.isValid) {
    // Логирование - побочный эффект, он остается на уровне сервиса
    logger.warn("Ошибка валидации email", {
      email,
      errors: result.errors,
    });
    throw new Error("Некорректный email");
  }

  // Дальнейшая логика регистрации...
}

Здесь чистая валидация легко тестируется и переиспользуется, а побочные эффекты изолированы в "обвязке".

Тестирование в условиях module-isolation

Как изоляция модулей помогает тестам

Когда модули изолированы:

  • их можно тестировать отдельно, подменяя зависимости;
  • тесты оказываются более устойчивыми к изменениям соседних модулей;
  • проще писать юнит-тесты, а не только интеграционные.

Подмена зависимостей через интерфейсы

Если модуль зависит от другого модуля через интерфейс, его легко "замокать". Давайте посмотрим на пример в TypeScript.

// billing/billing.port.ts
// Порт (контракт) платежного провайдера

export interface PaymentProvider {
  charge(amount: number, userId: string): Promise<void>;
}
// billing/billing.service.ts
// Сервис биллинга, зависящий от абстракции PaymentProvider

import type { PaymentProvider } from "./billing.port";

export class BillingService {
  // Внедряем зависимость через конструктор
  constructor(private readonly provider: PaymentProvider) {}

  async processOrder(userId: string, amount: number): Promise<void> {
    if (amount <= 0) {
      throw new Error("Сумма должна быть больше нуля");
    }

    // Вызываем абстрактный провайдер, не зная о деталях интеграции
    await this.provider.charge(amount, userId);
  }
}

Теперь давайте посмотрим, как можно протестировать BillingService, не поднимая реальный платежный сервис.

// billing/billing.service.test.ts
// Юнит-тест с подменой зависимостей

import { BillingService } from "./billing.service";
import type { PaymentProvider } from "./billing.port";

// Тестовая реализация PaymentProvider
class FakePaymentProvider implements PaymentProvider {
  public lastCharge: { amount: number; userId: string } | null = null;

  async charge(amount: number, userId: string): Promise<void> {
    // Сохраняем информацию о вызове для проверки в тесте
    this.lastCharge = { amount, userId };
  }
}

test("processOrder вызывает charge с правильными аргументами", async () => {
  const fakeProvider = new FakePaymentProvider();
  const billing = new BillingService(fakeProvider);

  await billing.processOrder("user-123", 100);

  // Проверяем, что модуль правильно использовал зависимость
  expect(fakeProvider.lastCharge).toEqual({
    amount: 100,
    userId: "user-123",
  });
});

Как видите, module-isolation через интерфейсы позволила протестировать модуль billing без реальной интеграции.

Тестирование по контракту

Если клиент и модуль общаются через явный интерфейс, вы можете:

  • написать тесты, которые гарантируют стабильность контракта (API- или контракт-тесты);
  • обнаруживать breaking changes до того, как они попадут к другим командам.

Например, если модуль user предоставляет REST API, вы можете описать контракт в OpenAPI и проверять реализацию на соответствие.

Изоляция модулей на уровне процесса и среды выполнения

Отдельные процессы и контейнеры

Иногда модуль изолируют до уровня:

  • отдельного процесса;
  • отдельного контейнера;
  • даже отдельной виртуальной машины.

Это крайняя форма module-isolation, подходящая для:

  • модулей с повышенными требованиями к безопасности (например, обработка платежных данных);
  • модулей, которые могут "падать", не влияя на остальную систему;
  • тяжелых вычислений.

Пример идеи на Node.js: запуск "опасного" кода в отдельном процессе через child_process.fork, ограничивая каналы взаимодействия.

// main.js
// Главный процесс запускает изолированный модуль как дочерний процесс

const { fork } = require("child_process");

const child = fork("./isolated-worker.js");

// Отправляем задачу дочернему процессу
child.send({ task: "heavy-computation", payload: { n: 10000 } });

// Получаем результат через сообщение
child.on("message", (message) => {
  // Здесь мы обрабатываем ответ из изолированного модуля
  console.log("Result from isolated module", message);
});
// isolated-worker.js
// Изолированный модуль, который общается с родителем только сообщениями

process.on("message", (message) => {
  // Здесь мы обрабатываем запрос, не имея прямого доступа к коду родителя
  if (message.task === "heavy-computation") {
    const result = heavyComputation(message.payload.n);
    // Отправляем результат обратно
    process.send({ ok: true, result });
  }
});

function heavyComputation(n) {
  // Здесь имитация тяжелого вычисления
  let sum = 0;
  for (let i = 0; i < n; i++) {
    sum += i;
  }
  return sum;
}

Дочерний процесс:

  • не разделяет память с основным;
  • общается только через сериализованные сообщения;
  • может быть перезапущен, не валя основное приложение.

Sandbox-изоляция и плагины

Если вы внедряете систему плагинов (например, в IDE или CRM), то изоляция модулей особенно важна:

  • плагины не должны иметь неограниченный доступ к внутренностям ядра;
  • изменения одного плагина не должны ломать другие.

Для этого часто:

  • запускают плагины в отдельных процессах или даже контейнерах;
  • предоставляют им ограниченный SDK (набор разрешенных API);
  • запрещают прямые обращения к внутренним структурам ядра.

Практические рекомендации по внедрению module-isolation

С чего начать в существующем проекте

Если у вас уже есть монолитный код с хаотическими зависимостями, вы можете двигаться поэтапно.

  1. Определите ключевые модули
    Например:

    • user;
    • billing;
    • auth;
    • notifications.
  2. Создайте "фасады" модулей
    Добавьте файлы user/index, billing/index и т.п., которые будут едиными точками входа.

  3. Начните ограничивать импорты
    Постепенно заменяйте прямые импорты внутренних файлов на импорты из "фасадов".

  4. Спрячьте детали реализации

    • в языках с модификаторами доступа: делайте внутренние сущности private или internal;
    • в JavaScript/TypeScript: просто не экспортируйте их из фасадов.
  5. Вводите интерфейсы на границах
    Для связей между ключевыми модулями введите интерфейсы (порты), чтобы уменьшить знание о деталях реализации.

Типичные ошибки при изоляции модулей

Обратите внимание на распространённые проблемы:

  1. "Утечка" внутренних типов наружу

    Например, модуль user внезапно начинает возвращать наружу сущность UserEntity, которая напрямую совпадает с таблицей в БД.
    Лучше вводить отдельные DTO и доменные типы.

  2. Циклические зависимости

    Модуль billing зависит от user, а user — от billing.
    Это ломает изоляцию и усложняет рефакторинг.
    В такой ситуации стоит:

    • вынести общие части в третий модуль (например, shared);
    • или ввести интерфейсы и инвертировать зависимость (Dependency Inversion).
  3. Смешивание слоёв внутри модуля

    В одном файле или классе:

    • HTTP-контроллер;
    • доменная логика;
    • SQL-запросы.

    Даже если это формально "один модуль", изоляция нарушается. Старайтесь разделять:

    • интерфейс (например, HTTP-контроллер);
    • доменную логику;
    • доступ к данным.
  4. Глобальные синглтоны вместо зависимостей

    Если модуль тянет синглтонов из глобального контейнера вместо того, чтобы принимать зависимости явно, вы теряете управляемость.
    Лучше внедрять зависимости в конструктор или фабрику модуля.

Checklist для проверки изоляции модуля

Вы можете задать себе несколько вопросов:

  • Явно ли определено, что является публичным API модуля?
  • Есть ли у модуля один или несколько четко обозначенных фасадов (entry points)?
  • Скрыта ли внутренняя структура файлов и типов от других модулей?
  • Есть ли контракты (интерфейсы) на границах между ключевыми модулями?
  • Удалось ли избежать циклических зависимостей?
  • Можно ли протестировать модуль отдельно, подменив внешние зависимости?

Если на большинство вопросов вы отвечаете "да", уровень module-isolation в вашем проекте уже достаточно хороший.


Изоляция модулей — это набор архитектурных и практических техник, которые помогают сделать систему устойчивее к изменениям, понятнее и безопаснее. Module-isolation опирается на несколько базовых идей:

  • четкие границы модулей и слоёв внутри них;
  • минимальный и устойчивый публичный интерфейс;
  • скрытие деталей реализации и состояния;
  • управление зависимостями в правильном направлении;
  • использование интерфейсов и контрактов на границах;
  • вынос побочных эффектов на "края" системы.

Применяя эти принципы шаг за шагом, вы сможете превратить даже "монолитную кашу" в структурированную систему, с которой удобно работать и развивать её дальше.

Частозадаваемые технические вопросы по теме статьи и ответы на них

Как постепенно ввести module-isolation в legacy проекте без больших переписываний?

Начните с малого
1 Выделите 2–3 очевидных модуля (например user и auth)
2 Создайте фасадные файлы index или api и постепенно переведите импорты на них
3 Запретите прямые импорты внутренних файлов через линтер или код-ревью
4 Постепенно скрывайте внутренние типы и функции делая их приватными или неэкспортируемыми
Так вы не ломаете существующий код а шаг за шагом вводите границы

Как обнаружить скрытые нарушения изоляции между модулями?

Используйте анализ зависимостей
1 Подключите инструмент наподобие dependency-cruiser madge или аналог для вашего языка
2 Опишите правила - какие директории могут импортировать какие
3 Регулярно запускайте анализ в CI
4 Фиксируйте нарушения постепенно начиная с самых критичных нарушающих архитектуру
Так вы получите карту реальных зависимостей и увидите где границы модулей "протекают"

Что делать если два модуля явно зависят друг от друга и образуют цикл?

Есть два основных подхода
1 Вынести общую часть в отдельный модуль shared или core на который будут зависеть оба исходных модуля
2 Ввести интерфейс и инвертировать зависимость - один модуль определяет контракт а реализацию этого контракта предоставляет другой модуль через внедрение зависимостей
Часто нужен комбинированный вариант с выделением общего ядра и интерфейсов

Как изолировать модуль в монорепозитории где много пакетов?

Используйте пакетные границы и правила импортов
1 Разделите код на независимые пакеты по доменным областям
2 Для каждого пакета определите public API через основной файл index
3 Запретите относительные импорты между пакетами вне этого API
4 Введите правила versioning между пакетами или используйте инструменты типа changesets
Так монорепо останется единым но модули будут вести себя как независимые библиотеки

Как ограничить доступ к модулю в микросервисной архитектуре?

Работайте через явные контракты
1 Выделите HTTP gRPC или message-based API как единственный способ общения с модулем-сервисом
2 Не давайте прямой доступ к его БД или внутренним очередям другим сервисам
3 Оформите контракт в виде OpenAPI Protobuf или схем событий и версионируйте его
4 Любые изменения сначала проверяйте на обратную совместимость
Так модуль-сервис остается изолированным а взаимодействие управляется через стабильный контракт

Стрелочка влевоПринципы FSD - как проектировать архитектуру фронтенда по фичам и слоямАрхитектурные слои в программных системах - слоеная архитектура просто и по делуСтрелочка вправо

Все гайды по Fsd

Открыть базу знаний

Отправить комментарий