Skip to main content
Preslav Rachev

Credits: Philip Estrada

A couple of months ago, Google released Service Weaver - a Go framework for developing so-called moduliths (modular monoliths). There is no exact definition of a modulith, but in my view, it is supposed to bridge the gap between monoliths and microservices. In simpler terms, a modulith allows the developer to develop and deploy an application as if it were a monolith and still be able to distribute it across a cluster of microservices at the push of a button.

Service Weaver
Service Weaver is a programming framework for writing and deploying cloud applications.

On paper, Service Weaver seems to be able to do just that - it introduces the concept of components that interact with but know very little about one another. A plain Go interface represents a component contract:

type Reverser interface {  
	Reverse(context.Context, string) (string, error)  
}

Each component has one or more concrete implementations too:

//go:generate go run github.com/ServiceWeaver/weaver/cmd/weaver@latest generate
type reverser struct {
	weaver.Implements[Reverser]
}

// Reverse is a concrete implementation of the Reverser interface 
func (r *reverser) Reverse(ctx context.Context, s string) (string, error) {
	// concrete implementation
}

The interesting part here is the embedding of weaver.Implements[Reverser] into reverser. Although it defies the core tenet of Go interfaces (interfaces and their implementations don’t need to know about one another), it serves an important purpose in Service Weaver. This is a marker for SW’s code generator to go and generate a whole bunch of type-safe Go code that does three things:

  1. Create a local stub for Reverser for when we run the application as a single monolith. Essentially, it “binds” Reverser to reverser and forwards all method calls to its concrete implementation.
  2. Create remote clients and servers for when (or if) we decide to split components into microservices. Thus, calling Reverser's Reverse(...) method turns into a whole chain of serializing the input, executing a network call, deserializing it on the other end, performing the desired action, and returning a response the entire way back. If that sounds a lot like CORBA, Erlang’s actor model, or Java’s Enterprise Beans (EJBs), that’s because it’s kind of what it is (or maybe not). In any case, that’s not what I am interested in right now. What interests me the most is p3.
  3. Initialize and inject component dependencies (concrete, or network proxies, depending on how we deploy).

A-ha! So SW is a network proxy code generator and a dependency injection (DI) container (sort of like Spring, but for Go). But how does that actually work?

Suppose our Reverser component wants to capitalize string inputs before returning them to the caller. For that, it will need a Capitalizer:

type Capitalizer interface {
	Capitalize(context.Context, string) (string, error)
}

To make use of the Capitalizer, our Reverser implementation needs to use another SW marker, called weaver.Ref:

type reverser struct {
	weaver.Implements[Reverser]
	cap weaver.Ref[Capitalizer]
}

Assuming that all the initialization is going through through SW, our Reverser can now make use of the Capitalizer implementation:

// Reverse is a concrete implementation of the Reverser interface 
func (r *reverser) Reverse(ctx context.Context, s string) (string, error) {
	// concrete implementation
	return cap.get().Capitalize(ctx, reversedStr)
}

Again, the concrete implementation of Capitalizer will change depending on whether we run our application as a monolith or as a distributed cluster of microservices, but our code will remain the same.

In a production scenario, Reverser and Capitalizer will be replaced by more practical examples - Storage (for DB operations), Mailer, various clients for 3rd-party API calls, or other business logic components.

It is important to note that this way of injecting components into one another is uncommon and not preferred in the Go community. As a Java Spring developer, I am used to using automated dependency injection, so I can see how the weaver.Ref part can become a thing of its own (even without the network proxy part), but that’s not what the majority of the Go community will agree with. Still, it is a matter of time to see where the future direction of SW will take, whether it will become a popular framework, and whether it will inspire a new wave of business application frameworks.

Have something to say? Join the discussion below 👇

Want to explore instead? Fly with the time capsule 🛸

You may also find these interesting