Goa makes it convenient to adopt the elegant monolith architecture, where all services are combined into a single codebase and run in a single process. This guide will walk you through the process of setting up elegant monoliths in Goa.
The key to building a monolithic application with Goa is the Endpoint
construct:
type Endpoint func(ctx context.Context, request any) (response any, err error)
Endpoints define both server and client side remotable functions. On the server side they wrap the actual business logic, while on the client side they wrap the transport layer.
For example given the following Goa design:
var _ = Service("calc", func() {
Method("multiply", func() {
Payload(func() {
Attribute("a", Int)
Attribute("b", Int)
})
Result(Int)
HTTP(func() {
GET("/multiply/{a}/{b}")
})
})
})
Server-side, Goa will generate a Go struct that lists all the services endpoints as well as a helper function to instantiate the struct given business logic:
// Endpoints wraps the "calc" service endpoints.
type Endpoints struct {
Multiply goa.Endpoint
}
// NewEndpoints wraps the methods of the "calc" service with endpoints.
func NewEndpoints(s Service) *Endpoints {
return &Endpoints{
Multiply: NewMultiplyEndpoint(s),
}
}
Client-side, Goa will generate a Go struct that exposes the service methods and can be constructed given the endpoints:
// Client is the "calc" service client.
type Client struct {
MultiplyEndpoint goa.Endpoint
}
// NewClient initializes a "calc" service client given the endpoints.
func NewClient(multiply goa.Endpoint) *Client {
return &Client{
MultiplyEndpoint: multiply,
}
}
// Multiply calls the "multiply" endpoint of the "calc" service.
func (c *Client) Multiply(ctx context.Context, p *MultiplyPayload) (res int, err error) {
// Omitted for brevity
}
The power of the Endpoint
construct is that it can be used independently of the
underlying transport layer. In particular a client can be constructed given
endpoints that are implemented in-memory, given the previous example:
package main
import (
"context"
"github.com/<your username>/calc/gen/calc"
)
func main() {
// 1. Instantiate the service
// NewService() is your function that returns a struct which implements the
// generated service interface
service := NewService()
// 2. Instantiate the server endpoints using the Goa generated NewEndpoints
endpoints := calc.NewEndpoints(service)
// 3. Instantiate the client using the Goa generated NewClient
client := calc.NewClient(endpoints.Multiply)
// 4. Use the client
res, err := client.Multiply(context.Background(), &calc.MultiplyPayload{A: 1, B: 2})
// ...
}
// NewService returns a new service instance.
func NewService() calc.Service {
// ...
}
This pattern makes it possible to benefit from the simplicity of deploying and operating monolithic applications while still enjoying the benefits of a modular and extensible architecture.
This pattern can be applied to entire systems that consist of multiple services
by following the layout described in the
Multiple Services guide. The main difference is in
the main
implementation which instantiates all the services instead of just
one.
Assuming a system which consists of both a users
and a products
services where
the products
service depends on the users
service:
// main.go
package main
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"goa.design/clue/log"
goahttp "goa.design/goa/v3/http"
genusersserver "myapi/services/users/gen/http/users/server"
genusers "myapi/services/users/gen/users"
"myapi/services/users"
genproductsserver "myapi/services/products/gen/http/products/server"
genproducts "myapi/services/products/gen/products"
"myapi/services/products"
)
func main() {
// Parse command line flags
var (
httpAddr = flag.String("http-addr", ":8080", "HTTP listen address")
debug = flag.Bool("debug", false, "Enable debug mode")
)
flag.Parse()
// Initialize context with logger
format := log.FormatJSON
if log.IsTerminal() {
format = log.FormatTerminal
}
ctx := log.Context(context.Background(), log.WithFormat(format))
if *debug {
ctx = log.Context(ctx, log.WithDebug())
log.Debugf(ctx, "debug mode enabled")
}
/*------------------------------------------*
* This section is specific to the monolith *
*------------------------------------------*/
// Create services and endpoints
usersSvc := users.NewUsers()
usersEndpoints := genusers.New(usersSvc)
// Create in-memory client for users service
usersClient := genusers.NewClient(usersEndpoints.Create, usersEndpoints.Find, usersEndpoints.Update, usersEndpoints.Delete)
productsSvc := products.NewProducts(usersClient)
productsEndpoints := genproducts.New(productsSvc)
// Create transport handlers
mux := goahttp.NewMuxer()
usersServer := genusersserver.New(usersEndpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil)
usersServer.Mount(mux)
productsServer := genproductsserver.New(productsEndpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil)
productsServer.Mount(mux)
// Log mounted endpoints
for _, m := range usersServer.Mounts {
log.Printf(ctx, "mounted %s %s", m.Method, m.Pattern)
}
for _, m := range productsServer.Mounts {
log.Printf(ctx, "mounted %s %s", m.Method, m.Pattern)
}
/*------------------------------------------*
* Rest is identical to any Goa application *
*------------------------------------------*/
// Create HTTP server
handler := log.HTTP(ctx)(mux) // Add logger to request context
httpServer := &http.Server{
Addr: *httpAddr,
Handler: handler,
}
// Handle shutdown gracefully
errc := make(chan error)
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
errc <- fmt.Errorf("signal: %s", <-c)
}()
ctx, cancel := context.WithCancel(ctx)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// Start HTTP server
go func() {
log.Printf(ctx, "HTTP server listening on %s", *httpAddr)
errc <- httpServer.ListenAndServe()
}()
<-ctx.Done()
log.Print(ctx, "shutting down HTTP server")
// Shutdown gracefully with a 30s timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := httpServer.Shutdown(ctx); err != nil {
log.Errorf(ctx, err, "failed to shutdown HTTP server")
}
}()
// Wait for shutdown
if err := <-errc; err != nil && !strings.HasPrefix(err.Error(), "signal:") {
log.Errorf(ctx, err, "server error")
}
cancel()
wg.Wait()
log.Print(ctx, "server exited")
}
This example creates a single HTTP server that serves the endpoints from all the services in the monolith. Any inter-service communication is handled by in-memory clients.