π― Introduction
In the world of frontend development, especially with React, it's common to see projects that start with a simple structure but, over time, become difficult to maintain, test, and scale. Business logic gets mixed with UI components, services are directly coupled to APIs, and tests become complex to write.
Sound familiar? If you've experienced these challenges, Hexagonal Architecture (also known as Ports and Adapters Architecture) combined with Vertical Slicing might be the solution you're looking for.
In this article, I'll show you how to implement this powerful architectural combination in React with TypeScript, using a real Todo management project as an example. We won't just talk about theory; we'll see real code, design decisions, and practical advantages that you can apply from day one.
π What is Hexagonal Architecture?
Hexagonal Architecture was proposed by Alistair Cockburn in 2005. Its main goal is to decouple business logic from technical implementation details.
Fundamental principles:
- Domain is the center: Business logic doesn't depend on anything external
- Dependency inversion: Outer layers depend on inner ones, never the reverse
- Ports and Adapters: Abstract interfaces (ports) and concrete implementations (adapters)
- Testable by design: Business logic can be tested without UI, without database, without APIs
Visual representation:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β INFRASTRUCTURE LAYER β
β ββββββββββββββββββββββββββββββββββββββββββββ β
β β APPLICATION LAYER β β
β β ββββββββββββββββββββββββββββββββββββ β β
β β β DOMAIN LAYER β β β
β β β β’ Entities β β β
β β β β’ Business Rules β β β
β β β β’ Domain Exceptions β β β
β β β β’ Ports (Interfaces) β β β
β β ββββββββββββββββββββββββββββββββββββ β β
β β β’ Use Cases / Services β β
β β β’ Application Logic β β
β ββββββββββββββββββββββββββββββββββββββββββββ β
β β’ Adapters (Repositories, API clients) β
β β’ UI Components (React) β
β β’ State Management (Zustand) β
β β’ Frameworks & Libraries β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
ποΈ Project Structure
Our application follows this folder structure:
src/
βββ features/
βββ todos/
βββ domain/ # π΅ Domain Layer
β βββ entities/
β β βββ todo.entity.ts
β βββ enums/
β β βββ todo-status.enum.ts
β βββ exceptions/
β β βββ domain.exception.ts
β β βββ invalid-todo.exception.ts
β β βββ invalid-todo-status-transition.exception.ts
β β βββ todo-not-found.exception.ts
β βββ ports/
β βββ todo.repository.ts
β
βββ application/ # π’ Application Layer
β βββ services/
β βββ todo.service.ts
β βββ dtos/
β βββ create-todo.dto.ts
β
βββ infrastructure/ # π‘ Infrastructure Layer
βββ adapters/
β βββ in-memory-todo.repository.ts
βββ factories/
β βββ todo-service.factory.ts
βββ stores/
β βββ todo.store.ts
βββ ui/
βββ components/
β βββ TodoItem.tsx
β βββ TodosList.tsx
β βββ TodoStatus.tsx
βββ pages/
βββ CreateTodoPage.tsx
βββ TodoDetailPage.tsx
βββ TodosPage.tsx
This structure clearly separates responsibilities and facilitates long-term maintenance.
π§© Vertical Slicing: Organization by Features
Besides Hexagonal Architecture, this project implements Vertical Slicing, an organizational pattern that structures code by complete features instead of technical layers.
What is Vertical Slicing?
Instead of organizing code by technical type (all components together, all services together), we organize it by business capability. Each feature contains everything needed to function independently:
β Horizontal (Traditional): β
Vertical (By Feature):
src/ src/
βββ components/ βββ features/
β βββ TodoItem.tsx βββ todos/
β βββ UserProfile.tsx β βββ domain/
β βββ ProductCard.tsx β βββ application/
βββ services/ β βββ infrastructure/
β βββ todoService.ts βββ users/
β βββ userService.ts β βββ domain/
β βββ productService.ts β βββ application/
βββ types/ β βββ infrastructure/
βββ Todo.ts βββ products/
βββ User.ts βββ domain/
βββ Product.ts βββ application/
βββ infrastructure/
Advantages of Vertical Slicing
1. High Cohesion, Low Coupling
Everything related to "Todos" is in one place. You don't have to jump between 10 different folders to understand a functionality.
2. Efficient Parallel Development
Two developers can work on todos and users simultaneously without merge conflicts, since they're in completely separate folders.
3. Easy to Remove or Extract
No longer need the todos feature? Delete the features/todos/ folder and nothing else breaks. Want to turn it into a micro-frontend? Extract it as an independent package.
4. Simplified Onboarding
A new developer can understand the complete feature by navigating a single folder, without needing to know the entire application.
5. Real Scalability
With 50+ features, it's much more manageable to have 50 feature folders than 50 files in each global technical folder.
6. Technical Decisions per Feature
Each feature can have its own implementation decisions. For example:
-
todosuses in-memory repository -
userscould use REST API -
analyticscould use WebSockets
Vertical Slicing + Hexagonal = πͺ
The combination of Vertical Slicing (organization by features) with Hexagonal Architecture (organization by layers within each feature) gives you the best of both worlds:
- Feature isolation (Vertical)
- Separation of concerns (Hexagonal)
- Predictable and consistent code
π΅ Layer 1: Domain (The Heart of the System)
The domain contains pure business logic, without external dependencies. It's the most important code and should be as stable as possible.
1.1 Enums: Todo States
// domain/enums/todo-status.enum.ts
export enum TodoStatus {
PENDING = "PENDING",
IN_PROGRESS = "IN_PROGRESS",
COMPLETED = "COMPLETED",
}
β οΈ Design decision: Using enums instead of magic strings prevents errors and makes code more maintainable.
1.2 Entity: Todo
The Todo entity is a behavior-rich class, not just a data container:
// domain/entities/todo.entity.ts
export interface TodoProps {
id: number;
title: string;
description: string;
status: TodoStatus;
createdAt: Date;
updatedAt: Date;
}
export default class Todo {
private readonly id: number;
private title: string;
private description: string;
private status: TodoStatus;
private readonly createdAt: Date;
private updatedAt: Date;
constructor(props: TodoProps) {
this.validateProps(props);
this.id = props.id;
this.title = props.title;
this.description = props.description;
this.status = props.status;
this.createdAt = props.createdAt;
this.updatedAt = props.updatedAt;
}
private validateProps(props: TodoProps): void {
if (!props.title.trim()) {
throw new InvalidTodoException("title cannot be empty.");
}
if (!props.description.trim()) {
throw new InvalidTodoException("description cannot be empty.");
}
}
// β
Getters to maintain immutability
getId(): number { return this.id; }
getTitle(): string { return this.title; }
getStatus(): TodoStatus { return this.status; }
// β
Setters with validation
setTitle(title: string): void {
if (!title.trim()) {
throw new InvalidTodoException("title cannot be empty.");
}
this.title = title;
this.touchUpdatedAt();
}
// β
Business logic: state transitions
start(): void {
if (this.status === TodoStatus.IN_PROGRESS) {
throw new InvalidTodoStatusTransitionException(
this.status,
TodoStatus.IN_PROGRESS
);
}
if (this.status === TodoStatus.COMPLETED) {
throw new InvalidTodoStatusTransitionException(
this.status,
TodoStatus.IN_PROGRESS
);
}
this.status = TodoStatus.IN_PROGRESS;
this.touchUpdatedAt();
}
complete(): void {
if (this.status === TodoStatus.PENDING) {
throw new InvalidTodoStatusTransitionException(
this.status,
TodoStatus.COMPLETED
);
}
if (this.status === TodoStatus.COMPLETED) {
throw new InvalidTodoStatusTransitionException(
this.status,
TodoStatus.COMPLETED
);
}
this.status = TodoStatus.COMPLETED;
this.touchUpdatedAt();
}
// β
Cloning to avoid mutations
clone(): Todo {
return new Todo({
id: this.id,
title: this.title,
description: this.description,
status: this.status,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
});
}
private touchUpdatedAt(): void {
this.updatedAt = new Date();
}
}
π¨ Advantages of this implementation:
- Centralized validation: Business rules are in one place
-
Immutability:
readonlyproperties andclone()method - Encapsulation: Private fields, controlled access through getters/setters
- Safe state transitions: You can't go from PENDING to COMPLETED directly
- Self-documenting: The code clearly explains which operations are valid
1.3 Domain Exceptions
Exceptions are typed and descriptive:
// domain/exceptions/domain.exception.ts
export class DomainException extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
// domain/exceptions/invalid-todo-status-transition.exception.ts
export class InvalidTodoStatusTransitionException extends DomainException {
constructor(from: string, to: string) {
super(`Cannot transition from "${from}" to "${to}".`);
}
}
β Benefit: Errors are clear and can be specifically handled in upper layers.
1.4 Ports: Interfaces
Ports are contracts defined by the domain but implemented by infrastructure:
// domain/ports/todo.repository.ts
import type Todo from "../entities/todo.entity";
export default interface TodoRepository {
create(todo: Todo): Promise<void>;
update(todo: Todo): Promise<void>;
delete(id: number): Promise<void>;
list(): Promise<Todo[]>;
findById(id: number): Promise<Todo | null>;
}
π Key: The domain defines WHAT it needs, not HOW it's implemented. This is dependency inversion in action.
π’ Layer 2: Application (Use Cases)
The application layer orchestrates domain logic. This is where services that implement use cases live.
2.1 DTOs (Data Transfer Objects)
DTOs define the data structure for communication between layers:
// application/services/dtos/create-todo.dto.ts
export interface CreateTodoDto {
title: string;
description: string;
}
2.2 Application Service
// application/services/todo.service.ts
import Todo from "../../domain/entities/todo.entity";
import { TodoStatus } from "../../domain/enums/todo-status.enum";
import { TodoNotFoundException } from "../../domain/exceptions/todo-not-found.exception";
import type TodoRepository from "../../domain/ports/todo.repository";
import type { CreateTodoDto } from "./dtos/create-todo.dto";
export default class TodoService {
constructor(private readonly todoRepository: TodoRepository) {}
async list(): Promise<Todo[]> {
return this.todoRepository.list();
}
async findById(id: number): Promise<Todo | null> {
return this.todoRepository.findById(id);
}
async create(dto: CreateTodoDto): Promise<Todo> {
const now: Date = new Date();
const todo: Todo = new Todo({
id: now.getTime(),
title: dto.title,
description: dto.description,
status: TodoStatus.PENDING,
createdAt: now,
updatedAt: now,
});
await this.todoRepository.create(todo);
return todo;
}
async delete(id: number): Promise<void> {
const todo: Todo | null = await this.todoRepository.findById(id);
if (!todo) throw new TodoNotFoundException(id);
await this.todoRepository.delete(id);
}
async start(id: number): Promise<Todo> {
const todo: Todo | null = await this.todoRepository.findById(id);
if (!todo) throw new TodoNotFoundException(id);
const cloned: Todo = todo.clone(); // π Avoid mutations
cloned.start();
await this.todoRepository.update(cloned);
return cloned;
}
async complete(id: number): Promise<Todo> {
const todo: Todo | null = await this.todoRepository.findById(id);
if (!todo) throw new TodoNotFoundException(id);
const cloned: Todo = todo.clone(); // π Avoid mutations
cloned.complete();
await this.todoRepository.update(cloned);
return cloned;
}
}
π‘ Important observations:
- Dependency injection: Repository is injected through the constructor
- Using clones: We never directly mutate the entity obtained from the repository
- Existence validations: We throw exceptions if the todo doesn't exist
- Thin layer: Only orchestrates, doesn't contain complex business logic
π‘ Layer 3: Infrastructure (Technical Details)
This layer contains concrete implementations of ports and all integration with the outside world.
3.1 Adapter: In-Memory Repository
// infrastructure/adapters/in-memory-todo.repository.ts
import Todo from "../../domain/entities/todo.entity";
import { TodoStatus } from "../../domain/enums/todo-status.enum";
import type TodoRepository from "../../domain/ports/todo.repository";
export default class InMemoryTodoRepository implements TodoRepository {
private todos: Todo[] = [
new Todo({
id: 1,
title: "Learn TypeScript",
description: "Study the basics of TypeScript and its features",
status: TodoStatus.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
}),
// ... more example todos
];
async create(todo: Todo): Promise<void> {
this.todos = [...this.todos, todo];
}
async update(updatedTodo: Todo): Promise<void> {
this.todos = this.todos.map((todo: Todo) =>
todo.getId() === updatedTodo.getId() ? updatedTodo : todo,
);
}
async delete(id: number): Promise<void> {
this.todos = this.todos.filter((todo: Todo) => todo.getId() !== id);
}
async list(): Promise<Todo[]> {
return this.todos;
}
async findById(id: number): Promise<Todo | null> {
const todo: Todo | undefined = this.todos.find(
(todo: Todo) => todo.getId() === id,
);
return todo ?? null;
}
}
π Key advantage: Tomorrow you can create an ApiTodoRepository that makes HTTP calls and only change the factory configuration. Your domain and application won't change a single line.
3.2 Factory Pattern: Dependency Injection
// infrastructure/factories/todo-service.factory.ts
import TodoService from "../../application/services/todo.service";
import type TodoRepository from "../../domain/ports/todo.repository";
import InMemoryTodoRepository from "../adapters/in-memory-todo.repository";
export default class TodoServiceFactory {
private static instance: TodoService | null = null;
static getInstance(): TodoService {
if (!this.instance) {
const repository: TodoRepository = new InMemoryTodoRepository();
this.instance = new TodoService(repository);
}
return this.instance;
}
}
π‘ Pattern applied: Singleton + Factory to manage service creation and configuration.
3.3 State Management with Zustand
// infrastructure/stores/todo.store.ts
import { create } from "zustand";
import type Todo from "../../domain/entities/todo.entity";
import type { CreateTodoDto } from "../../application/services/dtos/create-todo.dto";
import TodoServiceFactory from "../factories/todo-service.factory";
interface TodoStore {
todos: Todo[];
loadingTodos: boolean;
error: string | null;
listTodos: () => Promise<void>;
findTodoById: (id: number) => Promise<Todo | null>;
createTodo: (dto: CreateTodoDto) => Promise<void>;
deleteTodo: (id: number) => Promise<void>;
startTodo: (id: number) => Promise<void>;
completeTodo: (id: number) => Promise<void>;
}
const useTodoStore = create<TodoStore>()((set, get) => ({
todos: [],
loadingTodos: false,
error: null,
listTodos: async () => {
set({ loadingTodos: true, error: null });
try {
const todos: Todo[] = await TodoServiceFactory.getInstance().list();
set({ todos });
} catch (error) {
set({ error: (error as Error).message });
} finally {
set({ loadingTodos: false });
}
},
createTodo: async (dto: CreateTodoDto) => {
const todo: Todo = await TodoServiceFactory.getInstance().create(dto);
set({ todos: [...get().todos, todo] });
},
startTodo: async (id: number) => {
const updated: Todo = await TodoServiceFactory.getInstance().start(id);
set({
todos: get().todos.map((todo: Todo) =>
todo.getId() === id ? updated : todo,
),
});
},
completeTodo: async (id: number) => {
const updated: Todo = await TodoServiceFactory.getInstance().complete(id);
set({
todos: get().todos.map((todo: Todo) =>
todo.getId() === id ? updated : todo,
),
});
},
deleteTodo: async (id: number) => {
await TodoServiceFactory.getInstance().delete(id);
set({ todos: get().todos.filter((todo: Todo) => todo.getId() !== id) });
},
}));
export default useTodoStore;
β Zustand acts as an adapter between React and our application. Components don't know the internal architecture, only the store.
3.4 React Components
UI components are dumb and reusable:
// infrastructure/ui/components/TodoItem.tsx
export default function TodoItem({ todo }: TodoItemProps): JSX.Element {
const { startTodo, completeTodo, deleteTodo } = useTodoStore();
return (
<div className="group p-5 rounded-xl bg-white border border-slate-200">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<Link
to={`/todos/${todo.getId()}`}
className="text-lg font-semibold text-slate-800"
>
{todo.getTitle()}
</Link>
<p className="text-sm text-slate-500 mt-1">
{todo.getDescription()}
</p>
</div>
<TodoStatus status={todo.getStatus()} />
</div>
<div className="flex items-center gap-2 mt-4">
{todo.getStatus() === TodoStatus.PENDING && (
<button onClick={() => startTodo(todo.getId())}>
Start
</button>
)}
{todo.getStatus() === TodoStatus.IN_PROGRESS && (
<button onClick={() => completeTodo(todo.getId())}>
Complete
</button>
)}
<button onClick={() => deleteTodo(todo.getId())}>
Delete
</button>
</div>
</div>
);
}
π― Components as presenters: They only display data and delegate actions to the store.
π§ͺ Testing: The Great Benefit
Hexagonal Architecture shines especially in testing. Each layer can be tested in isolation.
Entity Test (Domain)
// tests/domain/todo.entity.test.ts
describe("Todo Entity", () => {
it("should transition from PENDING to IN_PROGRESS", () => {
const todo = new Todo({
id: 1,
title: "Test",
description: "Test description",
status: TodoStatus.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
});
todo.start();
expect(todo.getStatus()).toBe(TodoStatus.IN_PROGRESS);
});
it("should throw error when transitioning from PENDING to COMPLETED", () => {
const todo = createTodo({ status: TodoStatus.PENDING });
expect(() => todo.complete()).toThrow(
InvalidTodoStatusTransitionException
);
});
});
β No mocks, no complex setup: Pure business logic testing.
Service Test (Application)
// tests/application/todo.service.test.ts
describe("TodoService", () => {
let repository: TodoRepository;
let service: TodoService;
beforeEach(() => {
repository = new InMemoryTodoRepository();
service = new TodoService(repository);
});
it("should create a new todo with PENDING status", async () => {
await service.create({
title: "New Todo",
description: "New description"
});
const todos: Todo[] = await service.list();
expect(todos.length).toBe(4);
expect(todos[3].getStatus()).toBe(TodoStatus.PENDING);
});
it("should return a cloned instance when starting a todo", async () => {
const original: Todo | null = await service.findById(1);
const updated: Todo = await service.start(1);
expect(updated).not.toBe(original); // Different instances
expect(updated.getStatus()).toBe(TodoStatus.IN_PROGRESS);
});
});
β Lightweight integration test: We use the real (in-memory) repository without needing complex mocks.
UI Test (Infrastructure)
// tests/ui/todo-item.test.tsx
import { render, screen } from "@testing-library/react";
import { BrowserRouter } from "react-router";
import TodoItem from "../../features/todos/infrastructure/ui/components/TodoItem";
describe("TodoItem", () => {
it("should render todo with title and description", () => {
const todo = new Todo({
id: 1,
title: "Test Todo",
description: "Test Description",
status: TodoStatus.PENDING,
createdAt: new Date(),
updatedAt: new Date(),
});
render(
<BrowserRouter>
<TodoItem todo={todo} />
</BrowserRouter>
);
expect(screen.getByText("Test Todo")).toBeInTheDocument();
expect(screen.getByText("Test Description")).toBeInTheDocument();
});
});
π Advantages of This Architecture
1. Maintainability
- Each layer has a clear responsibility
- Code is easy to understand and modify
- Changes are contained within their layer
2. Superior Testability
- Business logic without external dependencies
- Fast and reliable tests
- You don't need to mount the entire app to test a business rule
3. Implementation Flexibility
- Change from in-memory to REST API: Only change the adapter
- Migrate from Zustand to Redux: Only affects infrastructure
- Replace React with Vue: Domain and application intact
4. Scalability
- Adding new features follows the same pattern
- Multiple developers can work in parallel
- Clear structure for large projects
5. Framework Independence
- React is just an implementation detail
- Business logic isn't coupled to any framework
- You can reuse domain and application in other contexts (mobile, backend, etc.)
6. Improves Collaboration
- Natural Domain-Driven Design
- Ubiquitous language between business and development
- Implicit documentation in the structure
7. Vertical Slicing: Natural Organization
- Code organized by business capabilities, not by technical type
- Self-contained features that are easy to navigate
- Parallel development without conflicts
- Decoupled features that can be removed or extracted without breaking anything
- Faster and more effective developer onboarding
π Comparison: Before vs. After
π΄ Traditional React Architecture
src/
βββ components/
β βββ TodoItem.tsx # 500 lines mixing UI, logic, validations
βββ pages/
β βββ TodosPage.tsx # Calls fetch() directly, inline validations
βββ utils/
β βββ api.ts # Loose functions
βββ types/
βββ Todo.ts # Simple interface without behavior
Problems:
- β Business logic scattered across components
- β Difficult to test (need to mount React)
- β Direct coupling to APIs
- β Duplicated validations
- β Backend changes require touching many files
- β Mixed features: to understand "todos" you must review multiple folders
- β Constant merge conflicts in the same files
π’ With Hexagonal Architecture
src/features/todos/
βββ domain/ # The truth of your business
βββ application/ # Use case orchestration
βββ infrastructure/ # Replaceable technical details
Benefits:
- β Centralized and testable logic
- β Simple and reusable components
- β Contained technological changes
- β Consistent validations
- β Fast and reliable tests
- β
Self-contained features in
features/todos/ - β Parallel development without collisions
- β Easy to remove or extract complete features
π― When to Use Hexagonal Architecture + Vertical Slicing
β Ideal for:
- Enterprise applications with complex business logic
- Projects that will scale over time
- Medium to large teams (Vertical Slicing facilitates parallel work)
- Applications with multiple integrations (APIs, localStorage, WebSockets)
- Projects requiring high test coverage
- When business logic must be shared (web, mobile, desktop)
- Applications with multiple independent features that can grow over time
β οΈ Consider alternatives if:
- It's a quick MVP that will likely be discarded
- Very simple application (landing page, static blog)
- Very small team with very tight deadlines
- Initial overhead is greater than expected benefits
π οΈ Project Technology Stack
- React 19: UI components
- TypeScript 5.9: Type safety and better DX
- Vite 7: Modern and fast build tool
- Vitest 4: Testing framework compatible with Vite
- React Router 7: Client-side routing
- Zustand 5: Minimalist state management
- TailwindCSS 4: Utility-first CSS (without classes, pure CSS)
π Conclusions
The combination of Hexagonal Architecture + Vertical Slicing in React is not just a passing fad; it's an investment in the long-term sustainability of your application. Although it requires more initial structure, the benefits in maintainability, testability, and flexibility are invaluable.
Key Takeaways:
- Domain First: Start by modeling your domain, not the UI
- Dependency Inversion: Outer layers depend on inner ones
- Ports and Adapters: Define interfaces, implement later
- Vertical Slicing: Organize by features, not by technical type
- Test, Test, Test: The architecture makes testing natural
- Pragmatism: Adapt the architecture to your context, not the other way around
Next Steps:
If you want to dive deeper, consider exploring:
- Value Objects to model more complex domain concepts
- CQRS (Command Query Responsibility Segregation) to separate reads and writes
- Event Sourcing if you need complete auditing
- Domain Events for communication between aggregates
- Specification Pattern for complex queries
π Acknowledgments
This article and the example project are the result of experience applying Clean Architecture and Domain-Driven Design in real React projects.
Special thanks to:
- Alistair Cockburn for conceiving Hexagonal Architecture
- Robert C. Martin (Uncle Bob) for popularizing Clean Architecture
- Eric Evans for Domain-Driven Design, which perfectly complements this architecture
- The React community for creating an incredible ecosystem that allows implementing these patterns
- The TypeScript developers for making JavaScript predictable and safe
- All developers who share knowledge and help raise the level of the industry
- And most importantly, to my beautiful family who are my driving force. Mazikeen, Brian, and FΓ‘tima, I love you β€οΈ
About the Author
Carlos Martinez is a Computer Systems Engineer and Full Stack Web Developer with 3 years of experience in the industry. He has worked implementing clean and scalable architectures in production projects, with a special focus on React and TypeScript.
This project was developed as educational material to demonstrate the practical implementation of Hexagonal Architecture in modern React applications, based on real experience applying these patterns in professional environments. The complete code is available on GitHub under MIT license https://github.com/Carlosgmdev/react-hexagonal.
Questions or Feedback?
If you implemented this architecture in your project or have questions, I'd love to hear about your experience. The best way to learn is by sharing knowledge.
Happy coding! ππ¨βπ»
π References and Additional Resources
- Hexagonal Architecture - Alistair Cockburn
- Clean Architecture - Robert C. Martin
- Domain-Driven Design - Eric Evans
- React Documentation
- TypeScript Handbook
Last updated: February 2026
#React #TypeScript #CleanArchitecture #HexagonalArchitecture #VerticalSlicing #SoftwareEngineering #Frontend #DomainDrivenDesign
Top comments (0)