---
title: "Developer Experience Cleanup: One Way to Do Things"
linkTitle: "Developer Experience Cleanup"
date: 2026-03-04
description: "Unified service creation, cleaner handler registration, and modular monolith support — the Go Micro DX overhaul"
aliases:
  - /blog/5
---
Go Micro has always prioritized getting out of your way. But over time, the API accumulated multiple ways to do the same thing — `micro.New()`, `micro.NewService()`, `service.New()`, three different handler registration patterns. If you're building something for AI agents or running a modular monolith, you shouldn't have to choose between equivalent APIs.

We've cleaned it up. Here's what changed and why.

## One Way to Create a Service

Before, there were three ways to create a service:

```go
// Old: three equivalent patterns
service := micro.New("greeter")                        // name only
service := micro.NewService(micro.Name("greeter"))     // options only
service := service.New(service.Name("greeter"))        // internal package
```

Now there's one canonical pattern:

```go
service := micro.New("greeter")
service := micro.New("greeter", micro.Address(":8080"))
```

Name is always the first argument. Options follow. `NewService` still works (it's deprecated, not removed), but every example, doc, and guide now uses `micro.New()`.

## Clean Handler Registration

Registering handlers used to require reaching through to the server:

```go
// Old: verbose, leaks abstraction
handler := service.Server().NewHandler(
    &TaskService{tasks: make(map[string]*Task)},
    server.WithEndpointScopes("TaskService.Create", "tasks:write"),
)
service.Server().Handle(handler)
```

Now `service.Handle()` accepts handler options directly:

```go
// New: clean, one call
service.Handle(
    &TaskService{tasks: make(map[string]*Task)},
    server.WithEndpointScopes("TaskService.Create", "tasks:write"),
)
```

For the common case with no options, it's just:

```go
service.Handle(new(Greeter))
```

## Modular Monoliths with Service Groups

Run multiple services in a single binary. Each service gets isolated state (server, client, store, cache) while sharing infrastructure (registry, broker, transport):

```go
users := micro.New("users", micro.Address(":9001"))
orders := micro.New("orders", micro.Address(":9002"))

users.Handle(new(Users))
orders.Handle(new(Orders))

g := micro.NewGroup(users, orders)
g.Run()
```

Start as a monolith, split into separate binaries when you need independent scaling. The Group handles signals and coordinated shutdown — all services start together and stop together.

## MCP Integration in One Line

Every service is automatically an MCP tool. Add a gateway alongside your service with one option:

```go
service := micro.New("greeter",
    micro.Address(":9090"),
    mcp.WithMCP(":3000"),
)

service.Handle(new(Greeter))
service.Run()
```

Your Go comments become tool descriptions. Your struct tags become parameter schemas. No glue code.

## Bug Fixes

- **Stop() error handling**: Previously, `Stop()` would silently swallow errors from `BeforeStop` hooks. Now all errors are properly propagated.
- **Store initialization**: Fatal-level log on store init failure changed to error-level — a store init failure shouldn't crash your service.
- **Service interface**: The internal implementation is now properly unexported. Users interact through the `service.Service` interface, not a concrete type.

## What This Means for You

If you're building new services, use `micro.New("name", opts...)` and `service.Handle()`. That's it.

If you have existing code using `micro.NewService()` or `service.Server().Handle()`, everything still works — we didn't break anything. But the docs, examples, and guides all point to the new patterns now.

The goal is simple: when someone asks "how do I create a service?", there should be exactly one answer.

See the updated [Getting Started guide](https://go-micro.dev/docs/getting-started.html) and the [agent demo](https://github.com/micro/go-micro/tree/master/examples/agent-demo) for working examples.
