What is Hexagonal Achitecture?
Hexagonal architecture was proposed by Alistair Cockburn
as an architecture that allows for flexible replacement as long as the internal/external connection standards called ports are met.
Pros and Cons of Hexagonal Architecture
pros
- Flexibility: Reduced dependencies on external systems or infrastructure, allowing components to be easily replaced or updated.
- Testability: The ability to independently test business logic helps improve quality and speed up development.
- Maintainability: Responsibilities are separated, making the code easier to understand and modify, and allowing for quicker response to changes.
cons
- Complexity: Implementation requires defining ports and implementing adapters, which increases the complexity compared to a simple implementation.
- Increased initial development time: Building the architecture initially requires time and effort.
That's all for the obvious explanation.
A hexagon is not a hexagon!
These kinds of various diagrams and similar explanations everywhere confuse us. These hexagonal drawings make it look more complicated. It's not a hexagon! It's completely unrelated.
Now I will explain what hexagonal architecture really is.
Before that, we need to understand what software is.
What is software?
In the world we live in now, there are various types of software. There are operating systems like Windows and Mac, and there are games ranging from simple 2D games to complex and flashy 3D games. Vending machines also have software. Even lights and light switches are a kind of program.
What do these various software have in common?
They have the process of Input -> Processing -> Output.
This is the essence of all software. It consists of input, processing, and output.
- Light
- Input: Switch
- Processing: Circuit
- Output: Light
- Vending machine
- Input: Purchase button
- Processing: Payment processing
- Output: Beverage dispensing
- Computer
- Input: Keyboard, mouse
- Processing: Games, videos
- Output: Monitor, speaker
All software has the structure described above. And the elements that connect input, processing, and output to each other are called ports. Things like USB 2.0
, USB Type-C
, HDMI 2.0
. To make it easy to understand, I'll explain using a computer as an example.
Ports are important.
Computers have various ports. These various ports are not for connecting just one single device.
You can connect various devices like mice, speakers, microphones, keyboards, etc. to a USB 2.0
port. Although they are different machines, they can be connected because they share and agree on port specifications and manufacture devices according to those port specifications.
Thanks to this, we don't have to disassemble and modify the computer every time we connect a different device to it. If every mouse and keyboard had different connection ports... we'd have to change the main unit to match the ports too.
That would be tremendously inconvenient. Therefore, we establish external and internal communication standards called ports and design and manufacture devices according to those ports.
The fatal attraction of ports Software is exactly the same.
- By defining port specifications and developing software according to those ports, when you want to replace it with new software, you can develop the new software according to the ports and easily swap it in. (Maintainability, Flexibility)
- Since the testing standard is the port specification, when you replace it with new software, the testing standards are the same as the previous software. Testing becomes easier. (Testability) Hexagonal architecture is what creates this form in software.
Components of Hexagonal Architecture
Hexagonal architecture doesn't have a fixed form. Using its core philosophy, it's modified slightly according to the project's characteristics. The commonly used terminology also varies slightly. This means different terms refer to the same object. If a team uses it, they should agree on terminology before developing.
Components The following 5 components are all there is to hexagonal architecture. Other things are added to these 5 components according to the project's characteristics.
- Presentation: Endpoints exposed externally for requests, connections, delivery, and communication to the application
- Inbound Port: Common specification definition that must be followed for requests, connections, delivery, and communication from external software to the application
- Outbound Port: Common specification definition that must be followed for requests, connections, delivery, and communication from the application to external software
- Inbound Adapter: Implementation that handles requests, connections, delivery, and communication from external software through Inbound Ports
- Outbound Adapter: Implementation that handles requests, connections, delivery, and communication to external software through Outbound Ports
Processing Flow of Hexagonal Architecture
This is the series of processes from input, processing, to output in hexagonal architecture.
Presentation Layer
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<UserDto> createUser(@RequestBody CreateUserRequest request) {
User user = userService.createUser(request.getName(), request.getEmail());
return ResponseEntity.ok(UserDto.from(user));
}
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(UserDto.from(user));
}
}
Presentation, that is, the part exposed to the outside. External entities make requests to this exposed part.
Inbound Port
public interface UserService {
User createUser(String name, String email);
User findById(Long id);
void deleteUser(Long id);
}
public class User {
private Long id;
private String name;
private String email;
private LocalDateTime createdAt;
public User(String name, String email) {
this.name = name;
this.email = email;
this.createdAt = LocalDateTime.now();
}
}
This is a form for user-related operations. For the createUser
operation, name
and email
are required and it returns a User
. No matter how you implement the interface
, the operation must work correctly. The Presentation
example operates as a Controller
that receives Restful API
, but whether it receives through gRPC
, Kafka Consume
, or TCP Socket
communication, calling the function through that interface guarantees the same operation. 😋
Inbound Adapter
@Service
@Transactional
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final EmailService emailService;
@Override
public User createUser(String name, String email) {
// 비즈니스 로직
if (userRepository.existsByEmail(email)) {
throw new DuplicateEmailException("Email already exists");
}
User user = new User(name, email);
User savedUser = userRepository.save(user);
// 이메일 발송
emailService.sendWelcomeEmail(savedUser.getEmail());
return savedUser;
}
@Override
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found"));
}
@Override
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
Inbound Adapter
is an implementation of Inbound Port
. Here, UserRepository
and EmailService
are Outbound Ports
.
Outbound Port
public interface UserRepository {
User save(User user);
Optional<User> findById(Long id);
boolean existsByEmail(String email);
void deleteById(Long id);
}
public interface EmailService {
void sendWelcomeEmail(String email);
void sendPasswordResetEmail(String email, String token);
}
If Inbound Port
is an interface for receiving requests from outside to the application, then Outbound Port
is an interface for the application to make requests to the outside. It's easy to understand if you think of it as an interface for external software to receive requests from the application. (The actual operation is slightly different)
Outbound Adapter
@Repository
public class JpaUserRepository implements UserRepository {
private final UserJpaRepository jpaRepository;
@Override
public User save(User user) {
UserEntity entity = UserEntity.from(user);
UserEntity saved = jpaRepository.save(entity);
return saved.toDomain();
}
@Override
public Optional<User> findById(Long id) {
return jpaRepository.findById(id)
.map(UserEntity::toDomain);
}
@Override
public boolean existsByEmail(String email) {
return jpaRepository.existsByEmail(email);
}
@Override
public void deleteById(Long id) {
jpaRepository.deleteById(id);
}
}
@Component
public class SmtpEmailService implements EmailService {
private final JavaMailSender mailSender;
@Override
public void sendWelcomeEmail(String email) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(email);
message.setSubject("Welcome!");
message.setText("Welcome to our service!");
mailSender.send(message);
}
@Override
public void sendPasswordResetEmail(String email, String token) {
// 비밀번호 재설정 이메일 로직
}
}
Outbound Adapter
is an implementation that implements Outbound Port
. It actually implements request logic suitable for each external software to operate.
Hexagonal architecture is actually simple.
This is all there is to it.
No matter what method is used to make requests, it can handle them all consistently.
No matter what DB you connect to, you can change the data that was retrieved from the DB to API and retrieve data through API instead.
Without changing the Business Logic
! (This is very important)
Hexagonal architecture:
- Has very strong resistance to environmental changes. (Hexagonal architecture separates and isolates the actual decision-making logic at the innermost part so that it is not affected by the external environment.)
- Since Business Logic is completely isolated, it is very suitable for writing
Unit Tests
, which are completely isolated tests.
The essence of hexagonal architecture
The Essence of Hexagonal Architecture Port
and Adapter
are just terminology, and everything explained above is one of many methods.
There are no absolutes.
The essence of hexagonal architecture can be said to be making logic that is truly important, needs to be meticulously tested, and must be accurate, strong and flexible without being affected by the external environment.
Usually when people think of hexagonal architecture, they tend to think of hexagons, but it's actually not related to hexagons.
What's really important is thoroughly separating what shouldn't change from what can be changed.
If you found this interesting, please give it a like!