Software design best practices, discovering package by layer, package by feature, and hexagonal architecture/ports and adapters.
In this blog post, I will explore how to structure our code and discuss best practices, covering three different approaches: package by layer, package by feature, and hexagonal architecture/ports and adapters with their pros and cons.
Before exploring different approaches for structuring code, we need to understand fundamental software design principles:
- Cohesion: Refers to the degree to which classes within a module are related to one another.
- Coupling: Refers to the degree of dependence between different modules.
- Modularity: Refers to the extent to which a software system is divided into separate and independent modules. Each module encapsulates a specific set of functionalities, and designed to work independently while interacting with each other through well-defined interfaces.
- Abstraction: Hiding implementation details and only exposing essential functionalities through interfaces.
- Separation of Concerns: Having distinct sections, each addressing a specific concern.
- Encapsulation: Bundle data and methods into a single module or class to hide internal details.
Let’s have a closer look at cohesion and coupling?
Cohesion describes how focused a piece of software is. It is very related to the Single Responsibility Principle.
- High cohesion means that classes within a module are closely related and share a common, well-defined purpose.
- Low cohesion means classes within a module are loosely related, lack a clear purpose with unrelated responsibilities.
A best practice to follow is to aim for high cohesion and loose coupling between modules.
Loose coupling is considered to be a sign of a well-structured computer system and a good design, and when combined with high cohesion, it results in high readability and maintainability.
Now, let’s explore different ways of structuring our code. First, I’ll cover package by layer, followed by package by feature, comparing the two. Afterwards, we’ll explore the port and adapter pattern.
Package by Layer
It represents a project structure where classes are organized into multiple layers, each responsible for a specific set of functionalities.
src
├── main
│ ├── java
│ │ └── com
│ │ └── app
│ │ ├── service
│ │ │ └── UserService.java
│ │ │ └── OrderService.java
│ │ │ └── ProductService.java
│ │ ├── domain
│ │ │ └── User.java
│ │ │ └── Order.java
│ │ │ └── Product.java
│ │ ├── repository
│ │ │ └── UserRepository.java
│ │ │ └── OrderRepository.java
│ │ │ └── ProductRepository.java
│ │ ├── controller
│ │ │ └── UserController.java
│ │ │ └── OrderController.java
│ │ │ └── ProductController.java
The typical layers include:
- Presentation Layer: This layer is responsible for handling user interactions and presenting information to the users. It often includes components related to user interfaces, controllers, and views.
- Service Layer: This layer contains the business logic and provides the data needed by presentation layer.
- Domain package: This package contains domain entities.
- Data Access Layer: This layer deals with the persistence and retrieval of data to/from database.
- Infrastructure package: This package provides services that support the operation of the application. It may include components for logging, configuration, security, and other cross-cutting concerns.
Here are some disadvantages of using Package by Layer:
- Low cohesion: unrelated classes are grouped into the same package.
- High coupling
- Poor Encapsulation: Most classes are public so we cannot make classes as package private, since they are needed in other layers.
- Low Modularity: Since each package contains classes related to a particular layer, it is difficult to break down the code into a Microservice later on.
- Poor Maintainability: Since classes are scattered across packages, it is difficult to find the class you are looking for.
- It promotes Database Driven Design rather than Domain Driven Design.
Package By Feature
It represents a structure where code is organized based on features or functionalities rather than layers. In this approach, each package represents a distinct and independent feature.
The goal is to group together all the components (such as controllers, services, repositories and domain classes) related to a particular feature in a single package.
src
├── main
│ ├── java
│ │ └── com
│ │ └── app
│ │ ├── user
│ │ │ ├── UserController.java
│ │ │ ├── UserService.java
│ │ │ └── UserRepository.java
│ │ ├── order
│ │ │ ├── OrderController.java
│ │ │ ├── OrderService.java
│ │ │ └── OrderRepository.java
│ │ ├── product
│ │ │ ├── ProductController.java
│ │ │ ├── ProductService.java
│ │ │ └── ProductRepository.java
Some of the benefits of using this structure:
- High cohesion
- Low coupling
- Strong encapsulation: Allows some classes to set their access modifier to package-private instead of public.
- High Modularity: Since each package contains classes related to a particular feature, it is easy to break code down into a Microservice later on.
- Maintainability: Reduces the need to navigate between packages since all classes needed for a feature are in the same package.
- Promotes Domain Driven Design
Ports and Adapters Pattern (Hexagonal Architecture)
Hexagonal Architecture also known as Ports and Adapters is a software architectural pattern introduced by Dr. Alistair Cockburn in an article he wrote in 2005.
The pattern promotes the isolation/separation of concerns by keeping the core business logic independent of external details and not tightly coupled to external dependencies such as databases, user interfaces, or external services.
This makes it easier to test, maintain, and evolve the system.
In this pattern:
- Domain / Core:
Represents the application’s business logic or domain(the heart of the application). - Ports:
Ports are interfaces defined by the core that allow interaction with external components. These can include interfaces for services, repositories, or any external dependency. - Adapters:
Adapters are implementations of the ports. They connect the core application to external components such as databases, user interfaces, and external services. Adapters can be specific to different technologies or protocols. - Primary actors:
Users of the system such as a webhook, a UI request, or a test script. - Secondary actors:
Used by the application, these services are either a Repository (for example, a database) or a Recipient (such as a message queue).
Hexagonal Shape:
The hexagonal shape symbolizes the idea that the core application is at the center, surrounded by adapters. This shape represents the clear separation between the core and its external dependencies.
The top level package structure should look like this:
src/main/
java
mina
dev
<servicename>
adapters
config
core
<ServiceApplication>.java
The root package should only contain packages: core
, adapters
and config.
- The
core
package contains all the domain logic of the service. It may contain sub packages. - Ports should be located in the core package: The ports are just interfaces declared by the core to be called or implemented by the adapters.
- The
adapters
package contains all adapter implementation code. It may contain sub packages to organize adapter code either per individual adapter or by technology. - The
config
package contains configuration classes used to connect the different components together.
Package dependency rules:
- The root package may depend on all other packages.
- The
config
package may depend oncore
andadapters.
- The
adapters
may depend oncore
but not onconfig.
Core
may not depend on any of the other packages.
You can find more details and example in my github repository: https://github.com/minarashidi/transfer-service-hexagonal-architecture/tree/main/src/main/java/com/rashidi/transferservice
I hope this post helped you to gain better understanding of the different code structures.
I’m eager to hear your thoughts and comments. Feel free to share your insights or ask any questions you may have. Thank you for reading!