MCPcopy Index your code
hub / github.com/LordMoMA/Hexagonal-Architecture

github.com/LordMoMA/Hexagonal-Architecture @main

Chat with this repo
repository ↗ · DeepWiki ↗ · + Follow
128 symbols 341 edges 28 files 7 documented · 5%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

🌌 Hexagonal Architecture with Go - A Thorough Exploration of Backend Engineering and Distributed System

This is the source code for the original article:

Hexagonal Architecture Deep Dive with PostgreSQL, Redis and Go Practices

Note that the codebase has evolved with more complexity than the article's example. The article is just a starting point for the project.

🏡 What is Hexagonal Architecture?

Hexagonal Architecture, also known as Ports and Adapters Architecture or Clean Architecture, is a software architecture pattern that promotes loose coupling between the application core (business logic) and external components such as user interface, database, and external services.

In Hexagonal Architecture, 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 the external components.

🔮 Hexagonal Architecture Components

Here are the components of the Hexagonal Architecture:

🎖 Core Business Logic:

The Core Business Logic is responsible for the main functionality of the application. This component represents the heart of the application and should be designed to be independent of any external dependencies. In Hexagonal Architecture, the Core Business Logic is implemented as a set of use cases that encapsulate the behavior of the application.

For example, if we are building a banking application, the Core Business Logic would include use cases such as creating an account, transferring funds, and checking account balance.

👯 Adapters:

The Adapters are responsible for connecting the Core Business Logic to the external world. Adapters can be of two types: Primary and Secondary.

🕺 Primary Adapter:

The Primary Adapter is responsible for handling incoming requests from the external world and sending them to the Core Business Logic. In Hexagonal Architecture, 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.

For example, in a banking application, the Primary Adapter would be an HTTP server that listens for incoming requests from clients, such as transferring funds or checking account balances, and then converts them into use cases that can be understood by the Core Business Logic.

🥁 Secondary Adapters:

The Secondary Adapters are responsible for interfacing with external dependencies that the Core Business Logic relies on. These dependencies can be databases, message queues, or third-party APIs. Secondary Adapters implement the ports defined by the Core Business Logic.

In a banking application, the Secondary Adapters would include database adapters that interface with the Core Business Logic to store and retrieve data about accounts, transactions, and other related information.

😈 Interfaces:

In software architecture, an interface refers to a contract or an agreement between two software components. It defines a set of rules or protocols that a component must follow in order to communicate with another component.

In the context of hexagonal architecture, interfaces play a critical role as they define the boundaries of the core business logic and the adapters. The core business logic only interacts with the adapters through their interfaces. This allows for easy replacement of adapters without affecting the core business logic.

For example, let's say you have an online shopping application that needs to process payments. You can define an interface for the payment gateway adapter, which outlines the methods that the core business logic can use to interact with the payment gateway.

You can then have multiple payment gateway adapters that implement this interface, such as PayPal, Stripe, and Braintree. The core business logic only interacts with the payment gateway adapters through their defined interface, allowing for easy replacement or addition of payment gateways without affecting the core business logic.

👨‍👦‍👦 Dependencies:

These are the external libraries or services that the application depends on. They are managed by the adapters, and should not be directly accessed by the core business logic. This allows the core business logic to remain independent of any specific infrastructure or technology choices.

🤡 Application structure (to be updated)

Now, let's dive into how to create a messaging backend that allows users to save and read messages. Hexagonal architecture adheres to strict application layout that needs to be implemented. Below is the application layout that we will use. This might look like a lot of work, but it will make sense as we move forward.

Structure of the Project

└── Hexagonal Architecture
   ├── cmd
   │   └── main.go
   ├── .env
   ├── images
   ├── go.mod
   ├── go.sum
   └── internal
       ├── adapters
       │   ├── cache
       │   │   └── cache.go
       │   │
       │   ├── handler
       │   │   ├── error_handler.go
       │   │   ├── login_handler.go
       │   │   ├── message_handler.go
       │   │   ├── stripe_handler.go
       │   │   ├── user_handler.go
       │   │   └── webhook_handler.go
       │   │
       │   ├── repository
       │   │   ├── apiCfg.go
       │   │   ├── db.go
       │   │   ├── message.go
       │   │   ├── payment.go
       │   │   └── user.go
       │   └── tests
       │       ├── integration
       │       └── unit
       │
       ├── config
       │    ├── config.go
       │    └── nginx.conf
       ├── core
       │   ├── domain
       │   │   └── model.go
       │   ├── ports
       │   │   ├── ports.go
       │   │   └── ports.go
       │   ├── services
       │   │   ├── message.go
       │   │   ├── payment.go
       │   │   └── user.go
       └── web

If you find any files other than the above are bugging you for understanding the structure, please feel free to delete them manually.

👺 To-dos:

  • ✅ Finish CRUD process of the messaging service
  • ✅ REST API Design with Gin
  • ✅ Add User service
  • ✅ Add JWT Authentication and Authorisation
  • ✅ Optimise error handling with clean code
  • ✅ Add Webhook to update membership status (idempotent)
  • ✅ Add a payment service
  • ✅ Work with Stripe API
  • ✅ postgreSQL as database
  • ✅ Redis as cache on users to improve performance
  • ✅ Add a new server for v2/payments endpoint
  • ✅ Add load balancer for server cluster
  • ✅ Add postgres image as test DB
  • 🥷🏻 Fix tests to use DB migration and pass the CI (data is persisted, and should be emptied after each test run)
  • ⌛️ Add telemetry to APIs
  • ⌛️ Add observability and monitoring to the /users/:id endpointd
  • ⌛️ Design wallet service
  • ⌛️ Design payment event service
  • ⌛️ Design a double-entry ledger system
  • ⌛️ Add Unit Test
  • ⌛️ Add Distributed services
  • ⌛️ Add URL Queries

How to keep the Test DB running and test the exact func with the triangle button in an IDE?

chmod +x testDB.sh

Now you can use the following command to start the test database:

./testDB.sh -t start

And use the following command to run tests and stop the test database afterwards:

./testDB.sh -t unit    # For unit tests
./testDB.sh -t integration    # For integration tests
./testDB.sh    # For all tests
./testDB.sh -t stop # stop the test database

🚀 Pros and Cons of using GORM in this project

GORM is a popular Object-Relational Mapping (ORM) library for the Go programming language that provides a convenient way to interact with databases, including PostgreSQL.

It provides a high-level, expressive and easy-to-use API for CRUD (Create, Read, Update, Delete) operations and supports several databases, including MySQL, PostgreSQL, SQLite, and others.

Whether GORM is better to use than directly using PostgreSQL depends on the specific use case. If you need a high-level, user-friendly API to interact with your PostgreSQL database, then GORM can be a great choice. On the other hand, if you have specific requirements for your database interactions or need to optimize performance for a large-scale application, then direct interaction with the PostgreSQL database using a lower-level database driver may be more appropriate.

In general, the use of an ORM can simplify and speed up development, especially for CRUD operations. However, it may introduce additional overhead and performance concerns.

please show me step by step of how payment service can work with Stripe API based on the following payment structure of hexagonal architecture, no need to use code.

🧠 Thoughts on the Payment Service and Stripe API Integration

If you already have an API endpoint that interacts with the Stripe API, you may not need a payment service in the Hexagonal Architecture. However, if you want to store payment data in your local database for future reference or analysis, you can create a payment service to handle this.

To get the payment data from the Stripe API endpoint, you can use webhooks to receive events from Stripe when a payment is made. You can then parse the webhook data and store the relevant payment information in your local database.

Alternatively, if you are using Stripe's checkout feature, you can use the client_secret that is returned when you create a PaymentIntent to confirm the payment after it is made. Once the payment is confirmed, you can retrieve the payment data from Stripe using the PaymentIntent ID and store it in your local database.

Overall, the payment service in the Hexagonal Architecture would be responsible for storing and retrieving payment data from the local database, and potentially for processing payments and interacting with the Stripe API via webhooks or other methods.

🌈 The Stripe's Checkout and PaymentIntent Confusion

When you are working with Stripe API for the first time, you might be confused about the difference between Stripe's Checkout and PaymentIntent. This is because they are both used to accept payments, but they serve different purposes and have different capabilities.

Stripe's Checkout and PaymentIntent are both features that allow you to accept payments through Stripe, but they serve different purposes and have different capabilities.

Stripe Checkout is a pre-built payment page that handles the payment process on behalf of the merchant. It allows merchants to quickly and easily integrate a payment flow into their website without having to build their own payment form. Stripe Checkout also supports many payment methods, including credit and debit cards, Apple Pay, and Google Pay.

PaymentIntent, on the other hand, is a flexible API that allows merchants to create and manage payment transactions programmatically. With PaymentIntent, merchants have more control over the payment process, including the ability to handle complex payment scenarios, such as partial payments, deferred payments, and payments with multiple payment methods. In other words, the PaymentIntent API is a low-level API that allows you to create and manage payment transactions programmatically. It is not a pre-built payment form like Stripe Checkout.

In summary, Stripe Checkout is a pre-built payment form that makes it easy for merchants to get started with Stripe payments, while PaymentIntent provides a more flexible and powerful API for handling payment transactions programmatically.

🌱 On Redis Parameters

In the Get method of RedisCache, the value parameter is defined as interface{} because it can take any type of value that is stored in the cache. The Get method is used to retrieve a value from the cache by providing the key. However, since the type of the value stored in the cache is unknown, it is specified as an empty interface interface{} which is a type that can hold any value.

🚇 On Redis Cache and PostgreSQL DB Dada Consistency

To maintain consistency between Redis Cache and PostgreSQL DB, you can implement a write-through or write-behind caching strategy.

In the write-through caching strategy, when data is updated in the PostgreSQL DB, it is also updated in the Redis Cache. This ensures that the data in the Redis Cache is always up-to-date with the latest data in the PostgreSQL DB. However, this approach can result in slower write performance due to the additional overhead of updating the cache.

In the write-behind caching strategy, data is first updated in the Redis Cache and then asynchronously updated in the PostgreSQL DB. This approach can improve write performance as data is first updated in the faster Redis Cache and then updated in the slower PostgreSQL DB. However, this approach can result in a temporary inconsistency between the Redis Cache and PostgreSQL DB.

Additionally, you can use a combination of database transactions and cache invalidation to ensure consistency. When a transaction is committed to the PostgreSQL DB, the cache is invalidated, and the next read from the cache will result in the latest data from the PostgreSQL DB.

Please read my article for more information on Cache Invalidation: The Hard Thing in Computer Science: Cache Invalidation

It's also important to ensure that the TTL (Time-to-Live) of the cached data is set appropriately. This ensures that the cached data is not stale and remains consistent with the data in the PostgreSQL DB.

However, in this project I want to keep this problem simple and easy to handle:

I will delete the cache data everytime the database is updated.

For example: when a user's email is updated in the database, I can delete the corresponding user's cache in Redis, so that the next time the user data is requested, it will be fetched from the database and cached again with the updated email. This ensures that the

Extension points exported contracts — how you extend this code

MessengerService (Interface)
(no doc) [3 implementers]
internal/core/ports/ports.go
MessengerRepository (Interface)
(no doc) [3 implementers]
internal/core/ports/ports.go
UserService (Interface)
(no doc) [3 implementers]
internal/core/ports/ports.go
UserRepository (Interface)
(no doc) [3 implementers]
internal/core/ports/ports.go
PaymentService (Interface)
(no doc) [3 implementers]
internal/core/ports/ports.go

Core symbols most depended-on inside this repo

HandleError
called by 37
internal/adapters/handler/error_handler.go
Get
called by 8
internal/core/ports/cache.go
LoadAPIConfig
called by 8
internal/adapters/repository/apiCfg.go
Delete
called by 5
internal/core/ports/cache.go
ReadUser
called by 5
internal/core/ports/ports.go
ValidateToken
called by 5
internal/adapters/handler/user_handler.go
ReadMessage
called by 4
internal/core/ports/ports.go
CreateUser
called by 4
internal/core/ports/ports.go

Shape

Method 77
Function 27
Struct 17
Interface 7

Languages

Go98%
TypeScript2%

Modules by API surface

internal/core/ports/ports.go32 symbols
internal/adapters/repository/user.go12 symbols
internal/core/services/user.go9 symbols
internal/adapters/handler/user_handler.go8 symbols
internal/core/services/message.go7 symbols
internal/adapters/handler/message_handler.go7 symbols
internal/logger/log_setup.go5 symbols
internal/core/domain/model.go5 symbols
internal/adapters/repository/message.go5 symbols
internal/adapters/handler/stripe_handler.go5 symbols
internal/adapters/cache/cache.go5 symbols
internal/core/ports/cache.go4 symbols

Datastores touched

postgresDatabase · 1 repos
template1Database · 1 repos
testdbDatabase · 1 repos

For agents

$ claude mcp add Hexagonal-Architecture \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact