👨‍💻 Un Tech Lead fullstack pour accélérer votre projet ?
Je suis dispo pour des missions React / Node / Cloud.

Auteur/autrice : techerjeansebastien

  • Implementing Hexagonal Architecture in Go: A Practical Guide

    In the ever-evolving landscape of software development, maintaining modularity and testability is paramount. One architectural pattern that stands out for achieving these goals is the Hexagonal Architecture, also known as the Ports and Adapters pattern. In this blog post, we’ll explore how to implement this architecture in Go (Golang) through a practical example: a simple User management system.

    The Hexagonal Architecture promotes a clear separation of concerns, ensuring that the core business logic remains independent of external systems such as databases, APIs, or user interfaces. This separation not only enhances maintainability but also facilitates testing and adaptability.

    Let’s dive into the concepts, structure, and implementation details of this architecture in our Go project.

    You can find all the project code sources here https://github.com/techerjeansebastienpro/go-hexa-example

    Understanding Hexagonal Architecture

    What is Hexagonal Architecture?

    Hexagonal Architecture, also known as the Ports and Adapters pattern, is an architectural style that aims to create loosely coupled application components that can be easily connected and disconnected. This architecture promotes a separation of concerns by isolating the core business logic from external systems such as databases, user interfaces, and third-party services. The core idea is to have a central « hexagon » or core, surrounded by various « adapters » that handle the communication with external entities through well-defined « ports. »

    In Hexagonal Architecture, the core consists of the application’s domain logic, including entities and use cases. The ports define the interfaces through which the core interacts with the outside world. Adapters are the implementations of these interfaces, providing the necessary code to connect the core to external systems. This structure allows the core to remain independent of the technical details of external systems, making the application more modular, maintainable, and testable.

      Benefits and Principles

      Benefits:

      1. Modularity: Hexagonal Architecture encourages the development of self-contained modules that are easier to manage and understand. Each module has a clear responsibility and can be developed and tested in isolation.
      2. Testability: By isolating the core business logic from external dependencies, Hexagonal Architecture makes it easier to write unit tests. Mocking external systems becomes straightforward, allowing for comprehensive testing of the core logic without requiring the actual systems.
      3. Flexibility and Maintainability: The clear separation between the core and external systems makes it simpler to replace or upgrade components. For instance, switching from one database to another or changing the user interface framework can be done with minimal impact on the core business logic.
      4. Independence from Frameworks: Hexagonal Architecture reduces the reliance on specific frameworks or technologies. The core logic remains pure and free from framework-specific code, making it easier to adapt to new frameworks or technological advancements.
      5. Enhanced Collaboration: This architecture facilitates better collaboration between different teams, such as front-end and back-end developers. Since the core logic is separated from the interfaces, different teams can work on their respective parts without interfering with each other.

      Principles:

      1. Separation of Concerns: Divide the application into distinct sections with clear responsibilities. The core should handle the business logic, while adapters should manage the technical details of interacting with external systems.
      2. Dependency Inversion: Depend on abstractions (ports) rather than concrete implementations. This principle ensures that the core logic does not directly depend on external systems, but rather on interfaces that can be implemented by any external system.
      3. Explicit Interfaces: Define clear and explicit interfaces (ports) for communication between the core and external systems. This approach makes the interactions well-defined and predictable.
      4. Independence from External Systems: Ensure that the core logic is not tightly coupled with any external system. The core should be self-sufficient and able to function without relying on specific external technologies or frameworks.
      5. Adaptability: Design the system in a way that makes it easy to replace or modify components without affecting the core logic. Adapters should be plug-and-play, allowing for seamless integration with different external systems.

      Project Structure

      Overview of the directory and file organization

      cmd/
        api/
          api.go
      internal/
        application/
          user_dto.go
          user_http_handler.go
        domain/
          user_entities.go
          user_ports.go
          user_service.go
        infrastructure/
          user_repository.go
          user_api.go
      pkg/
        models/
      

      Justification for the chosen structure

      Hexagonal Architecture principles don’t impose structure or models. We should apply them according to language limits and paradigms.

      This implementation is my interpretation of these principles according to my own experience with this language.

      I’m open for advice – do not hesitate to comment this blog post.

      Domain Layer Implementation

      Defining entities (User)

      internal/domain/user_entities.go

      On the business side, we manipulate entities. These objects represent the logic of the business.

        package domain
        
        type User struct {
        	ID    string
        	Email string
        }
        
        type FindOneById struct {
        	ID string
        }
        
        type FindOneByEmail struct {
        	Email string
        }
        
        type FindOneRequest struct {
        	FindOneById
        	FindOneByEmail
        }
        
        type CreateUser struct {
        	Email    string
        	Password string
        }
        
        

        Creating domain services and interfaces (ports)

        internal/domain/user_ports.go

        Here, we define the interfaces of our application. That’s what are the resources we can retrieve or actions we can do.

        package domain
        
        type UsersInput interface {
        	GetByID(ID string) (User, error)
        	CreateOne(request CreateUser) (User, error)
        }
        
        type UsersOuput interface {
        	FindOneById(request FindOneById) (User, error)
        	FindOneByEmail(request FindOneByEmail) (User, error)
        	InsertOne(request CreateUser) (User, error)
        }
        
        
        

        internal/domain/user_service.go

        This part aims to group all the business logic. If we have to add control checking or business rules …, we can add them here. It does not depend on any kind of technology and environment.

        package domain
        
        import (
        	"errors"
        )
        
        type UserService struct {
        	output UsersOuput
        }
        
        func NewUserService(out UsersOuput) *UserService {
        	return &UserService{output: out}
        }
        
        func (s *UserService) FindOne(request FindOneRequest) (User, error) {
        	if request.ID != "" {
        		return s.output.FindOneById(FindOneById{ID: request.ID})
        	}
        	if request.Email != "" {
        		return s.output.FindOneByEmail(FindOneByEmail{Email: request.Email})
        	}
        	return User{}, errors.New("invalid request")
        }
        
        func (s *UserService) Insert(request CreateUser) (User, error) {
        	return s.output.InsertOne(request)
        }
        
        

        Infrastructure: Implementing Interfaces

        Repository to access data sources

        internal/infrastructure/user_repository.go

        The repository is an implementation of the Output port UserOutput. As the domain service uses an output via dependency inversion, that implementation can be updated without affecting the business logic. In some cases, automated testing, for example, we can use a mocked repository.

        package infrastructure
        
        import (
        	"context"
        
        	"github.com/techerjeansebastienpro/go-hexa-example/internal/domain"
        	db "github.com/techerjeansebastienpro/go-hexa-example/pkg/models"
        )
        
        type UserRepository struct {
        	prisma *db.PrismaClient
        }
        
        func NewUserRepository(prisma *db.PrismaClient) *UserRepository {
        	return &UserRepository{
        		prisma: prisma,
        	}
        }
        
        func (r *UserRepository) FindOneById(request domain.FindOneById) (domain.User, error) {
        	ctx := context.Background()
        	foundUser, err := r.prisma.User.FindUnique(
        		db.User.ID.Equals(request.ID),
        	).Exec(ctx)
        
        	return domain.User{
        		ID: foundUser.ID,
        	}, err
        }
        
        func (r *UserRepository) FindOneByEmail(request domain.FindOneByEmail) (domain.User, error) {
        	ctx := context.Background()
        	foundUser, err := r.prisma.User.FindUnique(
        		db.User.Email.Equals(request.Email),
        	).Exec(ctx)
        
        	return domain.User{
        		ID: foundUser.ID,
        	}, err
        }
        
        func (r *UserRepository) InsertOne(request domain.CreateUser) (domain.User, error) {
        	ctx := context.Background()
        	createdUser, err := r.prisma.User.CreateOne(
        		db.User.Email.Set(request.Email),
        		db.User.Password.Set(request.Password),
        	).Exec(ctx)
        
        	return domain.User{
        		ID:    createdUser.ID,
        		Email: createdUser.Email,
        	}, err
        }
        
        

        API to expose your service

        internal/infrastructure/user_api.go

        The UserApi implements an input, UserInput, and uses the domain service. This part of the code does not depend on the environment and can be used by multiple « adapters » such as REST API / GraphQL API / gRPC … . We can mock the API in case of automated tests on those adapters.

        package infrastructure
        
        import (
        	"github.com/techerjeansebastienpro/go-hexa-example/internal/domain"
        )
        
        type UserApi struct {
        	userService domain.UserService
        }
        
        func NewUserApi(userService domain.UserService) *UserApi {
        	return &UserApi{
        		userService: userService,
        	}
        }
        
        func (a *UserApi) GetByID(ID string) (domain.User, error) {
        
        	return a.userService.FindOne(domain.FindOneRequest{
        		FindOneById: domain.FindOneById{
        			ID: ID,
        		},
        	})
        }
        
        func (a *UserApi) CreateOne(request domain.CreateUser) (domain.User, error) {
        	return a.userService.Insert(domain.CreateUser{
        		Email:    request.Email,
        		Password: request.Password,
        	})
        }
        
        

        Application: configure your application

        Create the first HTTP Handler

        package application
        
        import (
        	"github.com/gin-gonic/gin"
        	"github.com/techerjeansebastienpro/go-hexa-example/internal/domain"
        )
        
        type UserHttpHandler struct {
        	userInput domain.UsersInput
        	app       *gin.Engine
        }
        
        func NewUserHttpHandler(app *gin.Engine, userInput domain.UsersInput) *UserHttpHandler {
        	return &UserHttpHandler{
        		userInput: userInput,
        		app:       app,
        	}
        }
        
        func (u *UserHttpHandler) RegisterRoutes() {
        	u.app.GET("/users/:id", u.GetByID)
        	u.app.POST("/users", u.Create)
        }
        
        func (u *UserHttpHandler) GetByID(c *gin.Context) {
        	id := c.Param("id")
        	user, err := u.userInput.GetByID(id)
        	if err != nil {
        		c.JSON(500, gin.H{"error": err.Error()})
        		return
        	}
        
        	c.JSON(200, user)
        }
        
        func (u *UserHttpHandler) Create(c *gin.Context) {
        	var createUser domain.CreateUser
        	if err := c.ShouldBindJSON(&createUser); err != nil {
        		c.JSON(400, gin.H{"error": err.Error()})
        		return
        	}
        
        	user, err := u.userInput.CreateOne(createUser)
        	if err != nil {
        		c.JSON(500, gin.H{"error": err.Error()})
        		return
        	}
        
        	c.JSON(201, &UserDTO{
        		ID:    user.ID,
        		Email: user.Email,
        	})
        }
        
        
        package application
        
        type UserDTO struct {
        	ID    string `json:"id"`
        	Email string `json:"email"`
        }
        
        

        Complete Example

        Putting it all together in api.go

        cmd/api/api.go

        The program needs an entry point to bootstrap services and core systems. We instantiate all the services we need to run a specific HTTP service to handle external requests.

        package main
        
        import (
        	"fmt"
        
        	"github.com/gin-gonic/gin"
        	"github.com/spf13/viper"
        	"github.com/techerjeansebastienpro/go-hexa-example/internal/application"
        	"github.com/techerjeansebastienpro/go-hexa-example/internal/domain"
        	"github.com/techerjeansebastienpro/go-hexa-example/internal/infrastructure"
        	db "github.com/techerjeansebastienpro/go-hexa-example/pkg/models"
        )
        
        func main() {
        	envConfig()
        	fmt.Println(viper.GetString("DATABASE_URL"))
        	prismaClient := db.NewClient(
        		db.WithDatasourceURL(viper.GetString("DATABASE_URL")),
        	)
        	if err := prismaClient.Prisma.Connect(); err != nil {
        		panic(err)
        	}
        
        	defer func() {
        		if err := prismaClient.Prisma.Disconnect(); err != nil {
        			panic(err)
        		}
        	}()
        	userService := domain.NewUserService(infrastructure.NewUserRepository((prismaClient)))
        	api := infrastructure.NewUserApi(*userService)
        
        	app := gin.New()
        	application.NewUserHttpHandler(app, api).RegisterRoutes()
        
        	app.Run(":8080")
        
        }
        
        func envConfig() {
        	viper.SetConfigFile(".env")
        	viper.ReadInConfig()
        }
        
        

        Explanation of the overall flow

        This article provides an implementation overview, illustrating how the Hexagonal Architecture principles are applied in our Go project. Hexagonal Architecture is based on the principles of modularity, separation of concerns, and independence from external systems, ensuring that the core business logic remains isolated from technical details. While Go is a powerful and efficient language for implementing these principles, it does have some limitations, particularly in its support for generics. The limited generics in Go can restrict the ability to encapsulate certain functionalities, which might be more seamlessly achieved in languages with more advanced generic capabilities, such as Java or C#. Nonetheless, Go’s simplicity and strong typing make it a suitable choice for many applications, providing a clear and maintainable structure that adheres to the core concepts of Hexagonal Architecture.

        Conclusion

        In this blog post, we explored the implementation of Hexagonal Architecture in a Go project through a user management system. We started by understanding the core principles and benefits of this architectural pattern. Then, we delved into the detailed structure of our project, from defining the domain layer to implementing adapters and configuring the infrastructure. By maintaining a clear separation of concerns, Hexagonal Architecture not only enhances the modularity and testability of applications but also makes them more adaptable to changes. Whether you’re working on a small project or a complex system, this architecture can provide a robust foundation. We encourage you to experiment with this architecture in your projects and experience the benefits firsthand. Feel free to share your thoughts and questions in the comments below – we’d love to hear from you! Happy coding!

      1. Javascript est un langage incompris.

        Javascript est un langage incompris. Tant tôt aimé, souvent détesté, il reste le langage qui fait vivre le web.

        Image représentant un héro - Javascript - déconcerté

        Javascript est souvent considéré comme le moteur indispensable qui fait fonctionner le Web. Pourtant, malgré son rôle crucial dans le développement web, il est un langage très incompris. Certaines personnes l’adorent pour sa flexibilité et sa polyvalence, alors que d’autres le critiquent pour ses incohérences et ses complications. Dans ce blog, nous allons explorer pourquoi Javascript demeure un pilier essentiel du développement web, tout en soulevant les raisons de ses critiques et de ses louanges.

        1. Flexibilité et dynamisme

        Photo de Tikkho Maciel sur Unsplash

        Javascript est extrêmement flexible. Il permet aux développeurs de créer des interactions complexes sur les pages web en quelques lignes de code. Prenez l’exemple des frameworks populaires comme React ou Angular, qui utilisent Javascript pour construire des interfaces utilisateur dynamiques et réactives. Cette capacité à s’adapter et à répondre instantanément aux actions des utilisateurs est ce qui rend les sites modernes si engageants et interactifs.

        2. L’universalité

        Photo de Firmbee.com sur Unsplash

        Javascript est le seul langage de programmation qui s’exécute nativement dans les navigateurs web. Cela signifie que presque chaque appareil avec un navigateur internet peut exécuter Javascript sans avoir besoin d’installations supplémentaires. De plus, avec l’avènement de Node.js, Javascript s’est étendu au développement back-end, permettant aux développeurs d’utiliser un langage unique à travers la pile complète de développement, le développeur Fullstack Javascript. Cette ubiquité simplifie la formation des développeurs et l’interopérabilité des applications.

        3. La communauté Javascript et les ressources

        Photo de Hannah Busing sur Unsplash

        Javascript bénéficie d’une des plus grandes communautés de développeurs au monde. Le nombre impressionnant de frameworks, bibliothèques et outils disponibles est un témoignage de la vitalité de cette communauté. Cette richesse de ressources signifie que les développeurs peuvent souvent trouver une solution, une bibliothèque ou un plugin prêt à l’emploi pour presque n’importe quel problème qu’ils peuvent rencontrer, accélérant le développement et la rénovation technologique.

        4. Incohérences et difficultés

        Javascript apporte son lot de difficultés. Image représentant un enfant empilant des cube de bois
        Photo de Michał Parzuchowski sur Unsplash

        Malgré ses forces, Javascript n’est pas sans failles. Le langage a été critiqué pour ses incohérences, comme les façons parfois déroutantes dont il gère la coercition de types ou son modèle d’asynchronisme. Ces aspects peuvent rendre le débogage et le test des applications Javascript frustrants, même pour des développeurs expérimentés. Ceci est souvent source de réticence pour ceux qui sont habitués à des langages plus structurés comme Java ou C#

        5. Performances et optimisation

        Fusée qui décolle représentant les performance de Javascript
        Photo de NASA sur Unsplash

        Javascript a fait d’énormes progrès en termes de performances avec l’introduction de moteurs Javascript modernes comme V8 (utilisé dans Chrome et Node.js). Cependant, la gestion de la mémoire et les performances peuvent toujours poser problème, surtout dans les applications complexes. Les développeurs doivent souvent recourir à des techniques spécifiques pour optimiser leur code, ce qui peut augmenter la complexité des projets.

        Bien que contesté, Javascript fait partie du paysage du développement, et ce, pour durer.

        Javascript reste un pilier du développement web moderne, indissociable de l’expérience utilisateur interactive que nous attendons aujourd’hui des applications web. Malgré ses défis et les critiques qu’il peut essuyer, son évolution continue, portée par une communauté dynamique et innovante, prouve qu’il est bien plus qu’un simple langage de programmation. Comprendre et utiliser efficacement Javascript est essentiel pour tout développeur web aspirant à créer des applications modernes et performantes. Il est indéniable que JS est un langage vivant et évolutif, et c’est peut-être là, dans cette capacité à se réinventer constamment, que réside le vrai génie de ce langage.

      2. Why some tech hate Fullstack in France?

        Fullstack Developer is a unicorn. No one is an expert in everything.

        A unicorn that represents the vision of the community on the Fullstack developer

        In the French tech universe, the term « fullstack developer » is often met with skepticism or underestimated. Unlike the limited or negative perception that may exist in France, this role is widely recognized and valued, especially in the United States. A fullstack developer is a versatile asset, capable of handling both the front-end and back-end aspects of a project. Let’s explore why this specialization is not only relevant but essential in the current context of the technology industry.

        1 Definition and Scope of the Role

        Photo de Joshua Hoehne sur Unsplash

        The fullstack developer is responsible for developing an application or a website from start to finish. They must master front-end technologies, which affect what the user sees (HTML, CSS, JavaScript), as well as back-end technologies, which handle logic, database, and server management (such as Java, Python, Ruby). By having an overview of projects, the fullstack can quickly identify and solve various problems, optimizing the consistency and efficiency of development.

        2 – Versatility as an Asset

        An image that represent a fullstack developer doing lot of thing a the same time

        Being a fullstack developer means being versatile. This versatility allows for better adaptability in facing complex problems. Take, for example, startups in the United States where teams are often small: a fullstack developer is particularly valuable there because they can manage multiple aspects of a project without the need for different specialties, thus allowing for greater agility and rapid deployment.

        3 – Resource Economy

        Photo de Mathieu Stern sur Unsplash

        Employing fullstack developers often allows companies to reduce costs. Indeed, hiring a single person capable of handling multiple tasks reduces the need for separate specialists for front-end and back-end. A good example is a small startup company in the development phase of its web application: employing a fullstack can significantly decrease payroll expenses while simplifying work coordination.

        4 – Responding to Market Needs

        Photo de Robin Pierre sur Unsplash

        The global market, including France, is moving towards a demand for rapid deployability and efficiency in the development of technological products. The fullstack developer, with their ability to manage multiple phases of development, can accelerate prototyping and the implementation of solutions, thus effectively meeting this demand. Major American technology companies, like Google or Facebook, use teams of fullstack developers to maintain their lead in innovation.

        5 – Flexibility and Collaboration

        A group of workers that collaborate easily - a fullstack facilitate team working
        Photo de Windows sur Unsplash

        Working in fullstack also promotes a better understanding of the different aspects of a project, facilitating collaboration between teams. This integration of skills allows developers to make more informed and appropriate decisions, leading to more coherent and robust final products. In France, innovative companies are beginning to recognize these advantages by integrating fullstacks into their teams.

        The complexity of web technologies can be master. It is not pretentious about his know-how.

        Although poorly perceived by some within the French tech community, the role of a fullstack developer is crucial for effectively meeting the demands of a constantly evolving market. Their ability to navigate between front-end and back-end, their adaptability, and their global perspective on the project make them an indispensable element for both startups and large companies. It’s time to reconsider and value the fullstack specialization in France, taking a cue from successful practices across the Atlantic.