Реалізація Hexagonal Architecture для бекенду

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Hexagonal Architecture для бекенду
Складна
від 2 тижнів до 3 місяців
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Hexagonal Architecture для бекенду

Hexagonal Architecture (Ports & Adapters, автор — Alistair Cockburn) ізолює ядро додатку від зовнішніх деталей: фреймворків, БД, HTTP, черг повідомлень. Ядро визначає інтерфейси (Ports), зовнішні реалізації (Adapters) підключаються до них. Додаток однаково тестується як через HTTP, так і через CLI або тести напрямик.

Структура

src/
├── domain/              # Чистий домен: сутності, VO, бізнес-правила
│   ├── entities/
│   ├── value-objects/
│   └── exceptions/
├── application/         # Use Cases + Ports (інтерфейси)
│   ├── use-cases/
│   ├── ports/
│   │   ├── inbound/     # Driving ports — як викликають додаток
│   │   └── outbound/    # Driven ports — що додаток викликає
├── infrastructure/      # Adapters
│   ├── persistence/     # PostgreSQL, Redis adapters
│   ├── messaging/       # Kafka, RabbitMQ adapters
│   ├── http/            # REST, GraphQL controllers
│   └── external/        # Stripe, SendGrid adapters
└── main.ts              # Composition Root — складання адаптерів

Inbound та Outbound Ports

Inbound Port (Driving) — інтерфейс, через який зовнішній світ викликає додаток:

// ports/inbound/OrderUseCases.ts
export interface CreateOrderUseCase {
  execute(command: CreateOrderCommand): Promise<CreateOrderResult>;
}

export interface GetOrderUseCase {
  execute(query: GetOrderQuery): Promise<OrderDto | null>;
}

Outbound Port (Driven) — інтерфейс, який додаток використовує для зовнішніх залежностей:

// ports/outbound/OrderRepository.ts
export interface OrderRepository {
  findById(id: string): Promise<Order | null>;
  save(order: Order): Promise<void>;
}

// ports/outbound/PaymentGateway.ts
export interface PaymentGateway {
  charge(amount: Money, card: CardDetails): Promise<PaymentResult>;
  refund(paymentId: string, amount: Money): Promise<RefundResult>;
}

// ports/outbound/NotificationService.ts
export interface NotificationService {
  sendOrderConfirmation(order: Order, customer: Customer): Promise<void>;
}

Use Case (Application Core)

// application/use-cases/CreateOrderUseCase.ts
export class CreateOrderUseCaseImpl implements CreateOrderUseCase {
  constructor(
    private orderRepo: OrderRepository,        // outbound port
    private productRepo: ProductRepository,    // outbound port
    private paymentGateway: PaymentGateway,    // outbound port
    private notificationSvc: NotificationService // outbound port
  ) {}

  async execute(cmd: CreateOrderCommand): Promise<CreateOrderResult> {
    const order = Order.create(cmd.customerId);

    for (const item of cmd.items) {
      const product = await this.productRepo.findById(item.productId);
      if (!product) throw new ProductNotFoundError(item.productId);
      order.addItem(product, item.quantity);
    }

    order.submit();

    const payment = await this.paymentGateway.charge(
      order.total, cmd.paymentDetails
    );
    order.confirmPayment(payment.id);

    await this.orderRepo.save(order);

    // Fire and forget
    this.notificationSvc.sendOrderConfirmation(order, { id: cmd.customerId });

    return { orderId: order.id, status: order.status };
  }
}

Use Case нічого не знає про HTTP, PostgreSQL або Stripe — тільки про порти.

Adapters

Inbound HTTP Adapter (Express/Fastify):

// infrastructure/http/OrderController.ts
export class OrderController {
  constructor(private createOrder: CreateOrderUseCase) {}

  async create(req: Request, res: Response) {
    try {
      const result = await this.createOrder.execute({
        customerId: req.user.id,
        items: req.body.items,
        paymentDetails: req.body.payment
      });
      res.status(201).json(result);
    } catch (e) {
      if (e instanceof ProductNotFoundError) {
        return res.status(422).json({ error: e.message });
      }
      throw e;
    }
  }
}

Outbound PostgreSQL Adapter:

// infrastructure/persistence/PostgresOrderRepository.ts
export class PostgresOrderRepository implements OrderRepository {
  constructor(private db: Pool) {}

  async findById(id: string): Promise<Order | null> {
    const row = await this.db.query(
      'SELECT * FROM orders WHERE id = $1', [id]
    ).then(r => r.rows[0]);

    return row ? OrderMapper.toDomain(row) : null;
  }

  async save(order: Order): Promise<void> {
    const data = OrderMapper.toPersistence(order);
    await this.db.query(
      `INSERT INTO orders (id, customer_id, status, total, created_at)
       VALUES ($1, $2, $3, $4, $5)
       ON CONFLICT (id) DO UPDATE SET status=$3, total=$4`,
      [data.id, data.customerId, data.status, data.total, data.createdAt]
    );
  }
}

Outbound Stripe Adapter:

// infrastructure/external/StripePaymentGateway.ts
export class StripePaymentGateway implements PaymentGateway {
  private stripe: Stripe;

  async charge(amount: Money, card: CardDetails): Promise<PaymentResult> {
    const intent = await this.stripe.paymentIntents.create({
      amount: Math.round(amount.value * 100),
      currency: amount.currency.toLowerCase(),
      payment_method: card.tokenId,
      confirm: true
    });

    return { id: intent.id, status: intent.status };
  }
}

Composition Root

Єдине місце, де адаптери підключаються до портів:

// main.ts
const db = new Pool({ connectionString: process.env.DATABASE_URL });
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

const orderRepo = new PostgresOrderRepository(db);
const productRepo = new PostgresProductRepository(db);
const paymentGateway = new StripePaymentGateway(stripe);
const notificationSvc = new SendGridNotificationService(process.env.SENDGRID_KEY);

const createOrderUseCase = new CreateOrderUseCaseImpl(
  orderRepo, productRepo, paymentGateway, notificationSvc
);

const orderController = new OrderController(createOrderUseCase);

// Register routes
app.post('/orders', (req, res) => orderController.create(req, res));

Тестування

Головна перевага — тестування без реальних залежностей:

describe('CreateOrderUseCase', () => {
  it('створює замовлення та списує оплату', async () => {
    const mockOrderRepo = { save: jest.fn(), findById: jest.fn() };
    const mockPaymentGateway = {
      charge: jest.fn().mockResolvedValue({ id: 'pay_123', status: 'succeeded' })
    };
    const mockNotifications = { sendOrderConfirmation: jest.fn() };

    const useCase = new CreateOrderUseCaseImpl(
      mockOrderRepo, mockProductRepo, mockPaymentGateway, mockNotifications
    );

    const result = await useCase.execute(validCommand);

    expect(result.orderId).toBeDefined();
    expect(mockPaymentGateway.charge).toHaveBeenCalledWith(
      expect.objectContaining({ value: 150 }),
      validCommand.paymentDetails
    );
    expect(mockOrderRepo.save).toHaveBeenCalled();
  });
});

Юніт-тесту запускаються за мілісекунди. Немає запущеної БД або Stripe.

Терміни реалізації

  • Рефакторинг існуючої Express/Fastify програми до гексагональної архітектури — 2–4 тижні
  • Новий сервіс з нуля по hexagonal: 1 use case — 1–2 дні, повний модуль з 10+ use cases — 2–3 тижні
  • Налаштування тестової інфраструктури (mocks, in-memory adapters) — 3–5 днів