This is a high level guide on how to structure your Capstone project. Over the lifecycle of the project we will combine multiple patterns however the start of the project focuses on how to write a Hexagonal Architecture which is a software architecture pattern that promotes loose coupling between the business logic and external components such as user interface, database and external services.
The core of the application is isolated from external components and is instead accessed through a set of well-defined interfaces or ports. Adapters are then used to implement the required interfaces and integrate with external components.
Responsible for the main functionality of the application. This component represents the heart of the application and should be designed to be independent of external dependencies. This layer articulates the business rules and processes which are specific to the problem domain.
These are responsible for connecting Core Business Logic to the external world. They can be of two types: primary or secondary.
Responsible for handling incoming requests from the external world and sending them to the Core Business Logic. The Primary Adapter is typically an HTTP server, which receives HTTP requests from clients and converts them into requests that can be understood by the Core Business Logic.
Using the data-stager as one example, the primary adapter would be a HTTP server that listens for incoming requests from a stock exchange, such as creating a security event and converts them into use cases to be understood by Core Business Logic.
Responsible for interfacing with external dependencies that the Core Business Logic relies on. These may include databases, message queues or third-party APIs. They implement ports defined in the Core Business Logic.
Using the rating-service as one example, the secondary adapters would include broker adapters that interface with the Core Business Logic to store and retrieve data about signals and other related information.
Interfaces are defined by the core business logic and represent required functionality. It defines a set of rules or protocols that a component must follow in order to communicate with another component.
They are the boundaries of the core business logic and the adapters. The business logic only interacts with adapters through interfaces.
Using the rating-service as one example, the service will request and respond to the broker. There will be an interface for the broker component which outlines the methods that the core business logic can use to interact with the broker. Then, you can define multiple adapters that implement this interface and the core business logic only interacts with the adapters through their defined interface allowing the replacement or addition of broker types without impacting core business logic.
These are external libraries or services the application depends on. They are managed by adapters and should not be accessed by the core business logic. This allows for the logic to remain independent of specific infrastructure or technology choices.
In the context of a hexagonal architecture unit tests verify the behaviour of the core domain logic in isolation where integration tests would test the interactions and dependencies between the core logic and the adapters, such as the database or external API.
Within the provided hexagonal architecture structure, a unit folder containing a rating_result_service_test.go would contain the tests for the RatingResultService functions at the core level, testing their functionality in isolation from other parts of the system.
The integration folder containing a rating_result_integration_test.go file would simulate the interaction between the RatingResultService and the adapters, such as RatingResultRepository. These tests may use a real database or external API and aim to test the system as a whole.
|
Note
|
This is not doctrine, they are just opinionated examples. Other hexagonal structures MAY differ. |
Clean TypeScript services organise code with the following approach:
-
Application domain types such as
SecurityandRatingResult(structs) go in thesrc/core/domaindirectory. These are the collection of types which define what the application does without defining how it does it. -
Interfaces for managing the above types go in
src/core/ports. These are the interfaces adapters must follow. -
Everything is tied together in the
src/functionssubpackages, which represents working software. The implementation subpackages are loosely coupled so they need to be wired together by another package to make working software. -
Smaller packages don’t fall into the organisation listed above. For example, a
src/http/htmlpackage groups together HTML templates used by thehttppackage. -
src/core/servicesare pure functions or classes that coordinate domain and port interactions but do not depend on adapters.
└── Root
├── .env
├── images
├── package.json
├── package-lock.json
├── config
| └── nginx.conf
|
└── tests
│ ├── integration
│ └── unit
|
└── src
├── adapters
│ ├── cache
│ │ └── Memory.ts
│ │
│ ├── handler
│ │ ├── Error.ts
│ │ ├── Http.ts
│ │ └── ... cont
│ │
│ ├── repository
│ │ ├── ApiConfig.ts
│ │ ├── Db.ts
│ │ └── ... cont
│
├── core
│ ├── domain
│ │ └── Security.ts
│ │ └── RatingResult.ts
│ |
│ ├── ports
│ │ └── SecurityRepository.ts
│ │ └── RatingResultRepository.ts
│ |
│ ├── services
│ │ └── ... cont
| |
├── functions (optional: orchestrating entry points such as calling event handlers)You can apply this structure to almost any other language. Entrypoints will vary based on the calling pattern.
Various languages, such as Golang, will reject following this pattern in a strict manner without good reason to implement it. The above structure results in unnecessary complexity as a default stance. Following architectural patterns alone does not solve problems of code organisation. The proposed structure must be thought of as positions of reference and not positions of doctrine. The core idea of clean architecture is to decouple code and simplify testing and refactoring.
Go encourages a flatter project layout where interfaces are implicit. The goals of Clean Architecture are to decouple business logic from infrastructure where Go achieves this with less ceremony. Common Go conventions:
-
Implicit Interfaces: Interfaces are defined where they are needed, often inside core logic. This avoids creating large
portspackages unless multiple implementations exist. -
Project layout: Go projects use
cmd/,internal/andpkg/, grouping code by feature rather than hexagonal layer. -
Dependency injection: Dependencies are passed explicitly in constructors and wired in
main.go. -
Simplicity: The maxim 'Accept interfaces, return structs' reflects Go’s bias towards simple code and minimal indirection.
An example go implementation, in internal/securities/app/security_service.go:
type securityRepository interface {
FindSecurityForAugmentable(ctx context.Context, augmentable Augmentable) (Security, error)
}
type SecurityService struct {
repo securityRepository
}
func (s SecurityService) AugmentSecurity(ctx context.Context, actor auth.Actor, augmentable Augmentable) AugmentLineage {
if actor.Role != "Augmentor" {
return errors.Errorf("actor '%s' is trying to augment but cannot.", actor.UUID)
}
augmentResult := AugmentLineage{
Augmentable: augmentable,
Result: []
}
result, err := s.repo.FindSecurityForAugmentable(ctx, augmentable)
if err != nil {
augmentResult.Result = [ErrorResult{Result: err}]
return augmentResult
}
augmentResult.Result = [SuccessResult{Result: result}]
return augmentResult
}|
Note
|
Assume the adapters are defined in internal/securities/adapters/.
|
Now define a constructor in the same file to allow an adapter to be injected:
func NewSecurityService(
repo securityRepository
) SecurityService {
if repo == nil {
panic("missing securityRepository")
}
return SecurityService{
repo: repo
}
}Then in main.go inject the adapters:
securityRepository := adapters.NewPostgresSecurityRepository(client) // Assumed to be defined
securityService := app.NewSecurityService(securityRepository)The core logic is decoupled from infrastructure details whilst staying idiomatic to Go’s preference for simplicity and minimal layering.
As applications grow, you can revisit whether stricter separation is justified. The key is to preserve testability and independence of the core business logic without introducing unnecessary ceremony.