10

Pretty simple question, but googling brought me nowhere: What is the purpose of input ports in the hexagonal architecture?

We are doing Java and seem to have some misunderstandings regarding the input part of this pattern. We presume:

  • Domain Core / Business Logic: This part of the architecture describes our business and should be free of any framework specific / technical knowledge. It should be easily moveable from one technical infrastructure to another.
  • Outbound Ports: Defined by the core and implemented by the outbound adapter this is classical dependency inversion principle, so that the core stays free of any adapter-internal / technical details.

But what is the purpose of the incoming port? Every article on the web mentions it but they are all lacking an explanation for its purpose. Why should I care about defining this port and not let the input adapter access my business logic "directly"? Why this abstraction?

3
  • 3
    Input ports are there for the exact same reason as output ports are. To disconnect the "core" from technologies. To be clear, I don't see the benefit to either of those and I think this whole idea is actively hurting maintainability in most contexts, but if you do see it as advantageous in outputs, you should have no problems with inputs either. Commented May 19, 2022 at 8:49
  • 2
    For me it is not the same because the call direction is not the same: In case of an inbound event, the adapter calls the core itself, which should be agnostic on any technology. In case of an outbound event I have to define an interface (aka port) which gets implemented by an outbound adapter and gets called by the core itself. In that case i can prevent leakage of technology (e.g. classes of the network or persistence technology) to my core services Commented May 19, 2022 at 10:28
  • Why do all those questions in the newest timeline get downvoted so fast? What is this? "GateKeeping StackExchange" ? Commented May 19, 2022 at 13:23

4 Answers 4

11

Don't overthink it. The idea is that there's an inside and an outside of an application, and a boundary between them. The boundary ("ports") is not merely a line in some diagram, it consists of actual software elements and contracts/protocols associated with them. Despite what you may have read, these elements do not have to all be pure interfaces. There are some pros and cons there. A port could just be a concrete class behind which you can shift things around. I'll come back to that.

Now, what you strive for in your initial design, and then work towards as your software evolves and/or your understanding of what the requirements actually are changes, is to make the boundary relatively stable compared to both the inside and the outside (as in, arrive to a structure that doesn't change as frequently in comparison). This is because both the inside and the outside depend on the boundary to communicate with each other, so changes there would have rippling effects.

"Changes" could mean a number of different things - the outside might change because the UI is fiddly by nature, or because you want to rip out the UI and drive your application by a test suite, or because the format (but not the fundamental content) of the JSON response of some external API outside of your control changed. The inside might change because you've found a simpler, better way to represent something in code, or because you gained a new understanding of the requirements, or because you need to add another use case or something. Or because, to quote Ward Cunningham, "evolution of business practice wiggles around even those rules that 'must' apply".

Thinking in terms of input vs output ports is not actually that helpful. Alistair Cockburn, who first codified Hexagonal Architecture (a.k.a. Ports and Adapters), makes a distinction between primary ports and secondary ports, based on considerations about which of the two sides (outside, inside) initiates the interaction.

Primary ports are there to support outer components that call into the internal part of the application, so these outer components need to have a reference to the port. The port could just be an ordinary (concrete) class, an abstract class, an interface, or even just a function. What actually plays the role of the port there is the public (externally) visible members of the port class (or the signature + semantics of the function). You can choose (or evolve your design towards) any of these based on your needs and judgement. The important thing is that the port codifies the interaction, and that the core business logic happens behind it. Note that this sort of boundary is not limited to passing input. You can also use it to ask the application core for data or behavior.

Secondary ports allow the inside of the application to call into external components, so the ports (or rather, the implementations behind them) have to have a reference to these external components. Here's where you want to make use of dependency inversion, to make these references abstract and isolate the core. The abstraction of the external component is a part of the boundary (it's owned by the port); it is an important feature of the port's contract. If you decide to split the core (or parts of it) into a separate package, the port and the abstraction go inside the same package. Note that this reference to an external component does not have to be used for output only - the application's core can use this port to both push and pull data, and invoke behaviors.

I think the Internet's focus on input vs output ports comes from a common scenario where you have an outer component initiate some action by calling a primary port, providing some input, after which some internal processing happens that ultimately results in some output data being pushed through a secondary port.

Now, there's a number of reasons you might decide to have an input boundary / primary port represented by an interface. Maybe you've transparently composed your port out of multiple components in the style of the Composite pattern, and some of those components act as independent ports elsewhere. Maybe having an interface helps you with testing. Maybe you need some kind of multiple inheritance and you're using Java. Again, it's up to you.

Having an interface might help communicate to other devs in the team that it's a port, but the danger is that you'll come up with the wrong abstraction too early, and not course-correct in time, which is a problem. You know (or are in position to find out) the tendencies of your team best, so I guess you guys need to figure out what approach works for you.

In the end, don't adopt a certain convention because you're "supposed to" and because of "consistency" without first thinking it through as a team. Blind consistency is how you end up with spaghetti code.

P.S. If an adapter grows to be particularly complicated, you can at some point start treating it as its own small hexagonal component that's, in the grand scheme of things, dependent on the main hexagon, but locally has a small secondary port allowing you to inject either a reference towards the main app, or a mock.

2

Why should I care about defining this port and not let the input adapter access my business logic "directly"?

The short answer here is that when the internal structure of the business logic changes; the input adapter will break, and we don't want to work in a system where any change to the system propagates through the codebase like a stack of dominoes (or whack-a-mole, pick the analogy you prefer).

When using an intermediary interface, it acts as a firewall. The implementation is then free to change its internal structure as it sees fit, without repercussions. As long as it can keep complying with the interface without changing the interface; no other involved party will be affected by the business changing its internal structure.

You may be thinking to yourself "why interfaces? You could do the same with base method signatures in the class themselves", and that is technically correct (i.e. you are free to change the method body as long as the method signature doesn't change, the method signature itself behaves similar to an interface), but we've semantically come to use the distinction between interfaces/classes to inherently indicate the distinction between the interface/implementation of the business logic.

An additional reason for interfaces over classes is the ability to easily swap out one implementation for another, e.g. when mocking a dependency or in cases where there is more than one valid way to skin the proverbial cat.


Just for completeness' sake, when introducing a change that alters the interface (i.e. makes a breaking change, more than just expanding the interface), it is inevitable that both the consumer and implementer of the interface need to change. This is simply unavoidable and would have been similarly unavoidable if you hadn't been using interfaces.

That's why we call it a breaking change, after all.

3
  • You can't have "breaking changes" if you don't introduce the extra interface in the first place. Also, you're proposing technical interfaces here. That means the presentation, persistence and the "logic" is broken into separate pieces. Now what are the chances that a new feature will only touch exactly one of those? Commented May 19, 2022 at 8:43
  • @RobertBräutigam: (1) When a class' implementation changes (e.g. method signature), that also constitutes a breaking change, regardless of having an interface that more explicitly describes these method signatures or not. (2) Quite frankly, I'm tired of having this same discussion with you over and over. You clearly have a different approach to what is commonly discussed/approached in the field of OOP and class design. Different strokes for different folks, and that's all fine, but I'm tired of repeating the same conversation over and over. Not everything should devolve into a purism debate.
    – Flater
    Commented May 19, 2022 at 10:23
  • @RobertBräutigam: Sidestepping the same old discussion though, the issue is not so much with the implementation of new features but rather the changes made to existing features. This change might be introduced because the old code needs to be compatible with some new feature, but this is just one of many possible reasons. You'll also notice that in the last section I specifically point out that a new feature which only introduces an expansion to an existing interface is out of scope for a discussion on breaking changes for consumers.
    – Flater
    Commented May 19, 2022 at 10:28
1

Let's keep things as simple as they are (but not simpler). According to wikipedia:

Each component is connected to the others through a number of exposed "ports". Communication through these ports follow a given protocol depending on their purpose. Ports and protocols define an abstract API that can be implemented by any suitable technical means (e.g. method invocation in an object-oriented language, remote procedure calls, or Web services).

When Alistair Cockburn first shared about the hexagonal approach, he tried to be as general as possible in the wording. Indeed, the hexagons can be independently distribuable components such as microservices, where the ports will be implemented via "real" network ports. But they could also be application components embedded in a larger system, where the "port" is in fact just an exposed Java interface.

If your input adapter can be reached from the outside and you don't need to implement anything more, then the port is just a virtual view on what's already there: no need to code additional complexity, just to tick the boxes of some books ;-)

1

The port is an interface. You can implement that interface with an Interface (the keyword in your language), a fully abstract class, or (gasp) a concrete implementation just like you ask.

The first two help by formalizing (defining) the interface by moving everything that doesn’t directly impact the agreed on interface into another file.

A port also helps by being something that can be owned. That makes clear who can make breaking changes to it.

adapter calls the core itself, which should be agnostic on any technology

We’ll, no. The adapter can’t be agnostic. The point of the adapter is to let the other side be agnostic by taking care of the stuff that isn’t. That’s why you end up with one port and many adapters.

Not the answer you're looking for? Browse other questions tagged or ask your own question.