0

I have this project on github

My problem is that I'm not sure if I'm applying hexagonal architecture correctly.

I have made a project that is responsible for managing a to-do list, where I can work with an API or locally.

  • In the domain layer, I have the Task interface.
  • In the application layer, I have the use cases.
  • In the ports folder, I have an interface that, according to hexagonal architecture, is responsible for connecting to the infrastructure layer.
  • In the infrastructure layer, I have two files: one that connects to the API and another that works locally.

In the services layer, I have two files:

  • A factory service where I create two static methods to choose whether to work locally or with the API, and another where I implement the interface from the port.

I don't really understand why there are so many files to generate, for example, in the use cases. From the hook, I call the use cases, but I could also call the methods of taskService.

The problem is that I'm not sure if I'm doing it right. For example:

  • Where should I manage whether the calls were successful or if there was an error?

  • Where should I use Zod to check that the received data matches the interface?

Can someone explain to me if I'm doing this correctly?

1 Answer 1

-1

What you are doing is rather onion architecture or clean architecture, but it is definitely not hexagonal architecture.

The idea of isolating the domain layer from the infrastructure layer using an adapter pattern (with common adapters templates such as a Repository) is not hexagonal architecture. The very concept of domain as a unit of architecture was made popular by Evan's book on DDD 3 years after Alistair Cockburn coined the concept of hexagonal architecture.

Hexagonal architecture

In the ports folder, I have an interface that, according to hexagonal architecture, is responsible for connecting to the infrastructure layer.

No, hexagonal architecture - if that's what you actually want to implement - states that the port is responsible for connecting to the infrastructure itself, not the infrastrucure layer. A port isolates your application from code or technology that is beyond your control. By doing so, it allows you to decide whether you want to actually call that external reference or not in your context. This is why a port is defined as close as possible to 'technology': external libraies, network operations, system operations, filesystem, ...

In your case, your application has two ports:

  • When defering the http calls to axios;
  • When accessing the localStorage.

Since TypeScript supports structural typing, you don't even need to define interfaces for your ports. As long as you are able to inject somehow mocked axios or mocked localStorage objects, you're doing hexagonal architecture. However, creating an interface type can be a simple mean of creating and injecting mocks depending on your testing framework.

Hexagonal architecture also do not require you to write adapters. This is only needed if you don't like one of your externals interface. It allows you to create a port with a different structure, and a small set of instructions to convert from/to the actual one. It should be as small as possible as it is code that you write (and thus a mean of making your application fail) although usually not being in the system under test.

How you implement the rest of the application is beyond the scope of hexagonal architecture.

Clean/Onion architecture

I don't really understand why there are so many files to generate, for example, in the use cases. From the hook, I call the use cases, but I could also call the methods of taskService.

If your question was actually about the correct implementation of onion/clean architecture, rather than hexagonal per se, one aspect of the answer is that you are mixing patterns in your example. However, another important aspect of why it feels awkward is because your example is not a good example to practice onion/clean.

Your application manages todo objects, but these todo object are simple data bags without any business/domain logic. In consequence, your app is a mere CRUD of objects, and it makes it hard to implement architectural patterns based on domain-driven design when you don't have a domain logic.

A striking example of the problem is that you have an "udate todo use case". Updating and object is not domain-driven, it's crud-driven. A much better example of a domain-driven use case would be to "reschedule a task", with perhaps some validation rules. For instance: "the new target date cannot be in the past".

The service classes are IMHO a common antipattern of trying to separate logic from data in the domain. It should be the other way around.

Sorry I don't speak TypeScript very good, but here is an example in C#:

public class Task
{
  public DateTime Due { get; private set; }
  public bool Reschedule(DateTime to)
  {
    if(to < DateTime.now) { return false; }
    Due = to;
    return true;
  }
}

Then since the domain class should not know about the repository, hence the need for a RescheduleTaskUseCase class in the application layer:

public class RescheduleTaskUseCase(ITaskRepository repository)
{
  public Exception? Execute(Guid taskId, DateTime newDate)
  {
    if(repository.Find(taskId) is not Task task) { return new NotFoundException(); }
    if(!task.Reschedule(newDate)) { return new InvalidOperationException(); }
    repository.Update(task);
    return null;
  }
}

Now your app is free to implement the ITaskRepository however it see fit. Congratulations, you have now abstracted your infrastructure needs at the domain level (ITaskRepository) from it's actual implementations in the infra layer: LocalStorageRepository, AxiosRepository, RedisCached<AwsS3Repository>, ...

These implementations, as they transfer the responsibility of actually interacting with third party code or infrastructure, COULD use an hexagonal architecture to isolate the actual handoff. It would keep your repository implementation in the system under test if it has logic (for instance, session management, etc ...). However, if the implementation is a simple localStorage.Get(id), you could also consider the repository's interface as your hexagonal port.

Where should I manage whether the calls were successful or if there was an error?

What happens depending on whether the use case was successful or failed is a prsentation concern: it should be done in the UI layer: notify success or failure to the user, redirecting navigation to another page, etc ... If there are other 'infrastructure' concerns (logging failures, sending events, ...) should be included in the use case.

Where should I use Zod to check that the received data matches the interface?

This is an implementation detail of your repository class. It should go in the infrastructure layer.

Conclusion

As you can see, the purpose of both patterns are quite different. Hexagonal is about isolating your whole application from third party technology ('below' the infra layer), whereas onion/clean try to isolate the domain logic from the infra concerns.

Sign up to request clarification or add additional context in comments.

5 Comments

IMO, you're overthinking. Hexagonal architecture is nothing more than just the first attempt to implement the domain layer separation with the means of Dependency Inversion (DI). So-called adapters and ports are just fancy ways to refer to DI
I suggest you read Alistair Cockburn's "Hexagonal Architecture Explained". HA is about isolating your app from any external dependencies: "But Hexagonal Architecture has nothing to do with DDD. Hexagonal Architecture is simply a pattern that says: Put a driven port interface for any “real world thing” (driven actor) that the hexagon needs to talk to." Domain layers where coind by Eric Evans 2 years after HA, and isolating the domain layer using IOC was coined by Jeff Palermo's Onion Architecture 5 years after Evan's book.
So, as far as I understood, HA is just a general approach where you separate your core logic, whatever it may be, from the peripherals, i.e. external dependnecies. You use ports, i.e. just some interfaces which your core logic depends upon, and adapters, i.e. peripherals that implement those interfaces. And since HA is just a general thing, it can be used to implement DDD. Aaaand... That's it.
"HA is just a general approach where you separate your core logic, whatever it may be, from the peripherals". This general approach has a name: the Inversion of Control pattern. Using IOC to isolate app vs external is called Hexagenal Architecture. Using IOC to isolate domain vs non-domain is called Onion Architecture.
I would not call it IoC. A classical N-Layered architecture also uses IoC. Everything that relies on a system where you don't directly instantiate your dependencies but get them via some service locator, constructor or method is IoC.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.