Arhitectura Hexagonala | Un Ghid Practic pentru Aplicatii Moderne
Invata ce este arhitectura hexagonala, de ce conteaza si cum sa o implementezi. Un ghid complet cu exemple reale pentru construirea de software mentinabil si testabil.
Arhitectura hexagonala este un tipar de design software care izoleaza logica ta de business de baza de sistemele externe precum baze de date, API-uri si interfete de utilizator. Ideea este simpla: codul tau de domeniu nu ar trebui sa depinda niciodata de infrastructura. Infrastructura depinde de domeniu.
Tiparul a fost introdus de Alistair Cockburn in 2005. S-ar putea sa il auzi numit si “Ports and Adapters.” Ambele nume descriu acelasi concept.
De Ce Exista
Majoritatea aplicatiilor incep la fel. Logica de business se incurca cu interogari de baza de date, handlere HTTP si apeluri catre servicii terte. Functioneaza la inceput. Apoi baza de cod creste si se intampla trei lucruri:
- Testarea devine dureroasa. Nu poti testa regulile de business fara sa pornesti o baza de date sau sa faci mock la jumatate din framework.
- Schimbarea infrastructurii este riscanta. Inlocuirea unui furnizor de plati sau migrarea de la REST la GraphQL inseamna rescrierea logicii de business.
- Codul este greu de inteles. Nimeni nu poate spune unde se termina domeniul si unde incepe infrastructura.
Arhitectura hexagonala rezolva toate cele trei probleme prin impunerea unei singure reguli: dependentele indreptate intotdeauna spre interior.
Conceptele de Baza
Gandeste-te la aplicatia ta ca trei straturi concentrice.
Domeniul (Centrul)
Aceasta este logica ta de business. Functii pure, entitati, obiecte de valoare si servicii de domeniu. Are zero dependente de framework-uri, baze de date sau biblioteci externe. Vorbeste propriul limbaj.
Pentru un sistem e-commerce, acest strat contine concepte precum Order, Product, PricingRule si InventoryPolicy. Aceste obiecte nu stiu nimic despre SQL, HTTP sau JSON.
Porturi (Stratul de Mijloc)
Porturile sunt interfete care definesc cum interactioneaza lumea exterioara cu domeniul. Sunt contracte, nu implementari.
Exista doua tipuri:
- Porturi de intrare (driving). Definesc ce poate face aplicatia. Gandeste-te la ele ca cazuri de utilizare. “Plaseaza o comanda.” “Anuleaza un abonament.” “Genereaza o factura.”
- Porturi de iesire (driven). Definesc de ce are nevoie aplicatia din lumea exterioara. “Salveaza o comanda.” “Trimite o notificare.” “Obtine cursul de schimb.”
Porturile fac parte din stratul de domeniu. Sunt scrise in limbajul domeniului, nu in limbajul infrastructurii. Scrii OrderRepository, nu PostgresOrderDAO.
Adaptoare (Stratul Exterior)
Adaptoarele sunt implementarile concrete care conecteaza lumea reala la porturile tale.
- Adaptoarele de intrare traduc cererile externe in apeluri de domeniu. Un controller REST, un resolver GraphQL, o comanda CLI, un consumator de cozi de mesaje. Toate acestea sunt adaptoare de intrare.
- Adaptoarele de iesire implementeaza interfetele porturilor de iesire. Un repository PostgreSQL, un sender de email SMTP, un gateway de plati Stripe. Toate acestea sunt adaptoare de iesire.
Principiul cheie: adaptoarele depind de porturi. Porturile nu depind niciodata de adaptoare. Asta face ca intregul tipar sa functioneze.
Un Exemplu Concret
Sa spunem ca construiesti o functionalitate de plasare a comenzilor. Iata cum se descompun straturile.
Domeniu (entitati si reguli):
// domain/Order.ts
class Order {
readonly id: string;
readonly items: OrderItem[];
readonly status: OrderStatus;
calculateTotal(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.price.multiply(item.quantity)),
Money.zero()
);
}
confirm(): Order {
if (this.items.length === 0) {
throw new EmptyOrderError();
}
return new Order({ ...this, status: "confirmed" });
}
}
Port de intrare (interfata cazului de utilizare):
// ports/inbound/PlaceOrder.ts
interface PlaceOrder {
execute(command: PlaceOrderCommand): Promise<OrderConfirmation>;
}
Porturi de iesire (de ce are nevoie cazul de utilizare):
// ports/outbound/OrderRepository.ts
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
// ports/outbound/PaymentGateway.ts
interface PaymentGateway {
charge(amount: Money, method: PaymentMethod): Promise<PaymentResult>;
}
// ports/outbound/NotificationSender.ts
interface NotificationSender {
sendOrderConfirmation(order: Order): Promise<void>;
}
Serviciu de aplicatie (implementeaza portul de intrare):
// application/PlaceOrderService.ts
class PlaceOrderService implements PlaceOrder {
constructor(
private orders: OrderRepository,
private payments: PaymentGateway,
private notifications: NotificationSender
) {}
async execute(command: PlaceOrderCommand): Promise<OrderConfirmation> {
const order = Order.create(command.items);
const confirmed = order.confirm();
const payment = await this.payments.charge(
confirmed.calculateTotal(),
command.paymentMethod
);
if (!payment.successful) {
throw new PaymentFailedError(payment.reason);
}
await this.orders.save(confirmed);
await this.notifications.sendOrderConfirmation(confirmed);
return OrderConfirmation.from(confirmed, payment);
}
}
Adaptoare (infrastructura):
// adapters/inbound/OrderController.ts
class OrderController {
constructor(private placeOrder: PlaceOrder) {}
async handlePost(req: Request): Promise<Response> {
const command = PlaceOrderCommand.fromRequest(req.body);
const confirmation = await this.placeOrder.execute(command);
return Response.json(confirmation.toResponse(), { status: 201 });
}
}
// adapters/outbound/PostgresOrderRepository.ts
class PostgresOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
await this.db.query(
"INSERT INTO orders (id, items, status) VALUES ($1, $2, $3)",
[order.id, JSON.stringify(order.items), order.status]
);
}
async findById(id: string): Promise<Order | null> {
const row = await this.db.query("SELECT * FROM orders WHERE id = $1", [id]);
return row ? Order.fromPersistence(row) : null;
}
}
Observa cum PlaceOrderService nu mentioneaza niciodata PostgreSQL, HTTP sau vreun framework. Lucreaza pur cu concepte de domeniu si interfete de porturi.
Regula Dependentei
Aceasta este cea mai importanta regula. Daca nu retii nimic altceva din acest articol, retine asta:
Dependentele codului sursa trebuie sa fie intotdeauna indreptate spre interior, catre domeniu.
- Adaptoarele depind de porturi. Niciodata invers.
- Porturile depind de entitatile de domeniu. Niciodata de adaptoare.
- Domeniul nu depinde de nimic extern.
Asta se impune prin injectarea dependentelor. Radacina de compozitie (punctul de intrare al aplicatiei) conecteaza totul:
// main.ts (composition root)
const orderRepo = new PostgresOrderRepository(db);
const payments = new StripePaymentGateway(stripeClient);
const notifications = new EmailNotificationSender(mailer);
const placeOrder = new PlaceOrderService(orderRepo, payments, notifications);
const controller = new OrderController(placeOrder);
Stratul de domeniu nu importa niciodata din stratul de adaptoare. Daca vezi un import din adapters/ in interiorul domain/ sau ports/, ceva este gresit.
Structura Proiectului
O structura curata de foldere face granitele vizibile:
src/
domain/
Order.ts
OrderItem.ts
Money.ts
errors/
EmptyOrderError.ts
PaymentFailedError.ts
ports/
inbound/
PlaceOrder.ts
CancelOrder.ts
outbound/
OrderRepository.ts
PaymentGateway.ts
NotificationSender.ts
application/
PlaceOrderService.ts
CancelOrderService.ts
adapters/
inbound/
http/
OrderController.ts
cli/
PlaceOrderCommand.ts
outbound/
persistence/
PostgresOrderRepository.ts
payment/
StripePaymentGateway.ts
notification/
EmailNotificationSender.ts
main.ts
Fiecare dezvoltator din echipa poate privi acest arbore si intelege unde sa puna codul nou.
Beneficii pentru Testare
Aici arhitectura hexagonala chiar isi arata valoarea.
Testarea unitara a domeniului:
test("order calculates total correctly", () => {
const order = Order.create([
{ product: "Widget", price: Money.eur(10), quantity: 3 },
{ product: "Gadget", price: Money.eur(25), quantity: 1 },
]);
expect(order.calculateTotal()).toEqual(Money.eur(55));
});
Fara baza de date. Fara server HTTP. Fara framework de mocking. Logica pura, teste pure.
Testarea cazurilor de utilizare cu adaptoare false:
test("place order charges payment and saves", async () => {
const orders = new InMemoryOrderRepository();
const payments = new FakePaymentGateway({ alwaysSucceeds: true });
const notifications = new FakeNotificationSender();
const service = new PlaceOrderService(orders, payments, notifications);
const result = await service.execute({
items: [{ product: "Widget", price: 10, quantity: 2 }],
paymentMethod: { type: "card", token: "tok_test" },
});
expect(result.status).toBe("confirmed");
expect(orders.savedOrders).toHaveLength(1);
expect(payments.charges).toHaveLength(1);
expect(notifications.sent).toHaveLength(1);
});
Testezi intregul caz de utilizare fara sa atingi infrastructura reala. Adaptoarele false implementeaza aceleasi interfete de porturi, deci sunt interschimbabile cu cele reale.
Teste de integrare doar pentru adaptoare:
test("PostgresOrderRepository saves and retrieves", async () => {
const repo = new PostgresOrderRepository(testDb);
const order = Order.create([
{ product: "Widget", price: Money.eur(10), quantity: 1 },
]);
await repo.save(order.confirm());
const found = await repo.findById(order.id);
expect(found).toEqual(order.confirm());
});
Fiecare adaptor primeste propriul test de integrare. Daca adaptorul trece testul sau, si cazul de utilizare trece cu adaptoare false, intregul sistem functioneaza.
Cand sa Folosesti Arhitectura Hexagonala
Arhitectura hexagonala adauga structura si indirectare. Asta are un cost. Iata cand merita:
- Aplicatii cu viata lunga. Daca proiectul va fi intretinut ani de zile, investitia se amortizeaza rapid.
- Reguli de business complexe. Daca domeniul tau are logica non-triviala care trebuie testata temeinic.
- Puncte de intrare multiple. Daca aceeasi logica este apelata dintr-un API web, un CLI, o coada de mesaje si un cron job.
- Infrastructura s-ar putea schimba. Daca nu esti sigur daca vei ramane cu baza de date actuala, furnizorul cloud sau serviciile terte.
- Dezvoltatori multipli. Granitele clare reduc conflictele de merge si fac review-urile de cod mai rapide.
Cand sa o Sari
Nu fiecare proiect are nevoie de acest nivel de structura:
- Aplicatii CRUD simple. Daca aplicatia citeste si scrie in mare parte date cu logica de business minimala, indirectarea adauga cost fara beneficiu.
- Prototipuri si MVP-uri. Viteza conteaza mai mult decat arhitectura cand validezi o idee. Refactorizeaza mai tarziu.
- Scripturi si instrumente mici. Un instrument CLI de 200 de linii nu are nevoie de ports and adapters.
Cantitatea potrivita de arhitectura depinde de complexitatea problemei. Incepe simplu. Introduce granite hexagonale cand durerea de a nu le avea devine reala.
Greseli Comune
Scurgerea infrastructurii in domeniu. Daca entitatea ta Order are un decorator @Column sau o metoda toJSON(), infrastructura a patruns. Pastreaza-ti obiectele de domeniu curate.
Crearea de porturi care oglindesc infrastructura. Un port numit SqlQuery infrunge scopul. Porturile ar trebui sa descrie de ce are nevoie domeniul in limbaj de domeniu, nu cum functioneaza infrastructura.
Supra-abstractizare. Nu tot are nevoie de un port. Daca ai o functie utilitara care formateaza date, foloseste-o direct. Rezerva porturile pentru granitele reale de infrastructura.
Omiterea radacinii de compozitie. Fara un loc clar unde dependentele sunt conectate, tiparul se destrama. Fiecare dependenta ar trebui injectata, nu importata direct.
Hexagonal vs. Alte Tipare
Te-ai putea intreba cum se raporteaza arhitectura hexagonala la alte tipare cunoscute:
- Clean Architecture (Robert C. Martin): Foarte similar. Clean Architecture adauga mai multe straturi (entitati, cazuri de utilizare, adaptoare de interfata, framework-uri) dar ideea de baza este aceeasi: dependentele sunt indreptate spre interior.
- Onion Architecture (Jeffrey Palermo): De asemenea foarte similar. Onion Architecture foloseste inele concentrice cu domeniul in centru. Terminologia difera, dar regula dependentei este identica.
- Layered Architecture (N-tier): Abordarea traditionala in care straturile se stivuiesc vertical (prezentare, business, date). Diferenta cruciala: in arhitectura stratificata, stratul de business depinde de obicei de stratul de date. In arhitectura hexagonala, acea dependenta este inversata.
Toate cele trei tipare moderne (hexagonal, clean, onion) impartasesc acelasi principiu fundamental. Domeniul nu depinde de infrastructura. Difera mai ales in conventiile de numire si numarul de straturi pe care le prescriu.
Primii Pasi
Daca vrei sa aplici arhitectura hexagonala unui proiect existent, nu incerca sa refactorizezi totul deodata. Incepe cu un context delimitat sau o functionalitate:
- Identifica logica de business de baza in acea functionalitate. Extrage-o in functii pure sau clase fara dependente de framework.
- Defineste porturi pentru dependentele externe pe care le foloseste acea functionalitate. Creeaza interfete in limbajul domeniului.
- Muta codul de infrastructura existent in clase adaptor care implementeaza acele interfete.
- Conecteaza totul intr-o radacina de compozitie folosind injectarea dependentelor.
- Scrie teste pentru domeniu si cazurile de utilizare folosind adaptoare false.
Repeta pentru urmatoarea functionalitate. In timp, granitele hexagonale se vor raspandi natural in baza de cod.
Ganduri Finale
Arhitectura hexagonala nu inseamna urmarea unui sablon rigid. Este despre o idee simpla: protejeaza logica ta de business de haosul lumii exterioare. Bazele de date se schimba. Framework-urile ies din moda. API-urile sunt depreciate. Regulile tale de business ar trebui sa supravietuiasca tuturor acestor lucruri.
Tiparul functioneaza deoarece se aliniaza cu modul in care software-ul evolueaza de fapt. Cerintele se schimba constant. Infrastructura se schimba periodic. Dar regulile de baza ale afacerii tale tind sa fie cea mai stabila parte a sistemului. Construieste in jurul acelei stabilitati.
Daca construiesti un produs care trebuie sa dureze, arhitectura hexagonala este una dintre cele mai bune investitii pe care le poti face in baza ta de cod.
Ai nevoie de ajutor in proiectarea sau refactorizarea arhitecturii aplicatiei tale? Ia legatura cu noi. Ajutam echipele sa construiasca software care ramane mentinabil pe masura ce creste.