Skip to content

Instantly share code, notes, and snippets.

@blinkinglight
Created February 26, 2026 14:38
Show Gist options
  • Select an option

  • Save blinkinglight/818277c3096f339db9dc3f4eb77e2023 to your computer and use it in GitHub Desktop.

Select an option

Save blinkinglight/818277c3096f339db9dc3f4eb77e2023 to your computer and use it in GitHub Desktop.

Microservice Specification

Name

Echo Service

Purpose

The Echo Service is a simple CQRS-based microservice that receives commands or queries via NATS, processes them (which in this case means doing nothing but passing the message along), and returns the original message.

Architecture

  • NATS: Message transport.
  • CQRS: Command Query Responsibility Segregation.
    • Commands: The service accepts and echoes back command messages.
    • Queries: The service accepts and echoes back query messages.
  • Datastore: None (no persistence; purely in-memory echo).
  • Framework: Go with Flowbite UI for documentation generation.

Requirements

  1. Command Handling

    • Accepts command subjects (cmd.<aggregate>.<action>).
    • Echoes back the received command object.
  2. Query Handling

    • Accepts query subjects (query.<module>.<entity>).
    • Echoes back the received query object.
  3. Error Handling

    • If an invalid subject is received, return a structured error response.
  4. Serialization

    • Use JSON for all messages.
  5. Documentation

    • Generate HTML docs using Templ templating engine.

Interfaces

Protobuf / JSON Schema

Define a single message type that can be used for both commands and queries.

syntax = "proto3";

package echo;

message Envelope {
    string id = 1;
    string aggregate = 2;
    string action = 3;
    string module = 4;
    string entity = 5;
    map<string, any> payload = 6;
}

NATS Subjects

  • Commands: cmd.<aggregate>.<action>
  • Queries: query.<module>.<entity>

Response Format

  • On success: Return the original message.
  • On error: Return a structured error object:
{
    "error": {
        "code": "invalid_request",
        "message": "Invalid command format"
    }
}

Test-Driven Development (TDD)

Unit Tests

Ensure the service correctly parses subjects, echoes back messages, and handles errors.

Integration Tests

Verify the service responds over NATS without relying on external dependencies.


Implementation

Dependencies

Add these to your go.mod:

require (
    github.com/nats-io/nats.go v1.20.0
    github.com/yourorg/datastar/v2 v2.0.0 // hypothetical Datastar package
    github.com/yourorg/templ v1.0.0     // templ templating engine
    github.com/samber/option v1.4.0      // optional values
)

Code Structure

  • cmd/echo: Core business logic.
  • internal/nats: NATS client wrapper.
  • internal/model: Echo message model.
  • docs: Generated Flowbite documentation.

Step 1: Write Unit Tests

Test File: cmd/echo/command_test.go

package echo_test

import (
    "encoding/json"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "github.com/nats-io/nats.go"
    "github.com/yourorg/datastar/v2"
    "github.com/yourorg/templ"
    "github.com/yourorg/templ/pkg/html"
    "github.com/yourorg/templ/pkg/markdown"
    "github.com/yourorg/templ/pkg/text"
    "github.com/yourorg/templ/pkg/tmpl"
    "github.com/yourorg/templ/pkg/ui"
    "github.com/yourorg/templ/pkg/util"
    "github.com/yourorg/cmd/echo"
    "github.com/yourorg/internal/model"
)

func TestHandleCommand(t *testing.T) {
    ctx := context.Background()
    repo := &datastar.MockRepository{}
    svc := echo.New(repo)

    cmd := &model.Envelope{ID: "1", Aggregate: "user", Action: "create", Payload: map[string]interface{}{
        "name": "John Doe",
    }}

    reply, err := svc.HandleCommand(ctx, "cmd.user.create", cmd)
    require.NoError(t, err)
    assert.Equal(t, cmd, reply)
}

func TestHandleCommand_InvalidSubject(t *testing.T) {
    ctx := context.Background()
    repo := &datastar.MockRepository{}
    svc := echo.New(repo)

    cmd := &model.Envelope{ID: "1", Aggregate: "user", Action: "create", Payload: map[string]interface{}{
        "name": "John Doe",
    }}

    _, err := svc.HandleCommand(ctx, "unknown.subject", cmd)
    require.Error(t, err)
    assert.Contains(t, err.Error(), "invalid request")
}

Test File: cmd/echo/query_test.go

package echo_test

import (
    "encoding/json"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "context"
    "github.com/nats-io/nats.go"
    "github.com/yourorg/datastar/v2"
    "github.com/yourorg/templ"
    "github.com/yourorg/templ/pkg/html"
    "github.com/yourorg/templ/pkg/markdown"
    "github.com/yourorg/templ/pkg/text"
    "github.com/yourorg/templ/pkg/tmpl"
    "github.com/yourorg/templ/pkg/ui"
    "github.com/yourorg/templ/pkg/util"
    "github.com/yourorg/cmd/echo"
    "github.com/yourorg/internal/model"
)

func TestHandleQuery(t *testing.T) {
    ctx := context.Background()
    repo := &datastar.MockRepository{}
    svc := echo.New(repo)

    q := &model.Envelope{ID: "1", Module: "user", Entity: "profile", Payload: map[string]interface{}{
        "id": "123",
    }}

    reply, err := svc.HandleQuery(ctx, "query.user.profile", q)
    require.NoError(t, err)
    assert.Equal(t, q, reply)
}

func TestHandleQuery_InvalidSubject(t *testing.T) {
    ctx := context.Background()
    repo := &datastar.MockRepository{}
    svc := echo.New(repo)

    q := &model.Envelope{ID: "1", Module: "user", Entity: "profile", Payload: map[string]interface{}{
        "id": "123",
    }}

    _, err := svc.HandleQuery(ctx, "unknown.subject", q)
    require.Error(t, err)
    assert.Contains(t, err.Error(), "invalid request")
}

Step 2: Implement Core Logic

File: cmd/echo/service.go

package echo

import (
    "context"
    "fmt"
    "github.com/nats-io/nats.go"
    "github.com/yourorg/datastar/v2"
    "github.com/yourorg/internal/model"
)

type Service struct {
    repo datastar.Repository
}

func New(repo datastar.Repository) *Service {
    return &Service{repo: repo}
}

func (s *Service) HandleCommand(ctx context.Context, subject string, cmd *model.Envelope) (*model.Envelope, error) {
    if !isValidCommandSubject(subject) {
        return nil, fmt.Errorf("invalid request: %w", ErrInvalidRequest)
    }

    return cmd, nil
}

func (s *Service) HandleQuery(ctx context.Context, subject string, q *model.Envelope) (*model.Envelope, error) {
    if !isValidQuerySubject(subject) {
        return nil, fmt.Errorf("invalid request: %w", ErrInvalidRequest)
    }

    return q, nil
}

func isValidCommandSubject(s string) bool {
    return s == "cmd.user.create" || s == "cmd.user.update" || s == "cmd.user.delete"
}

func isValidQuerySubject(s string) bool {
    return s == "query.user.profile" || s == "query.user.list"
}

Error Constants

var (
    ErrInvalidRequest = fmt.Errorf("invalid request")
)

Step 3: Implement NATS Integration

File: internal/nats/client.go

package nats

import (
    "context"
    "log"
    "time"

    "github.com/nats-io/nats.go"
    "github.com/yourorg/cmd/echo"
)

type Client struct {
    nc *nats.Conn
}

func Dial(url string) (*Client, error) {
    nc, err := nats.Connect(url, nats.Timeout(1*time.Second))
    if err != nil {
        return nil, err
    }
    return &Client{nc: nc}, nil
}

func (c *Client) Subscribe(ctx context.Context, subject string, handler nats.MsgHandler) error {
    _, err := c.nc.QueueSubscribe(subject, "", handler)
    if err != nil {
        return err
    }
    return nil
}

func (c *Client) Request(ctx context.Context, subject, reply string, msg []byte) ([]byte, error) {
    return c.nc.RequestWithContext(ctx, subject, reply, msg)
}

func (c *Client) Close() error {
    return c.nc.Close()
}

File: cmd/echo/nats.go

package echo

import (
    "context"
    "github.com/nats-io/nats.go"
    "github.com/yourorg/internal/nats"
    "github.com/yourorg/internal/model"
)

const (
    natsReplyPrefix = "reply."
)

func RunNATS(ctx context.Context, svc *Service, nc *nats.Client) error {
    subCmd := "cmd.>"
    subQry := "query.>"

    if err := nc.Subscribe(subCmd, func(m *nats.Msg) {
        go handleCommand(ctx, svc, m)
    }); err != nil {
        return err
    }

    if err := nc.Subscribe(subQry, func(m *nats.Msg) {
        go handleQuery(ctx, svc, m)
    }); err != nil {
        return err
    }

    return nil
}

func handleCommand(ctx context.Context, svc *Service, m *nats.Msg) {
    cmd, err := decodeEnvelope(m.Data)
    if err != nil {
        log.Printf("decode error: %v", err)
        return
    }

    reply, err := svc.HandleCommand(ctx, m.Subject, cmd)
    if err != nil {
        sendError(ctx, nc, m.Reply, err)
        return
    }

    sendReply(ctx, nc, m.Reply, reply)
}

func handleQuery(ctx context.Context, svc *Service, m *nats.Msg) {
    q, err := decodeEnvelope(m.Data)
    if err != nil {
        log.Printf("decode error: %v", err)
        return
    }

    reply, err := svc.HandleQuery(ctx, m.Subject, q)
    if err != nil {
        sendError(ctx, nc, m.Reply, err)
        return
    }

    sendReply(ctx, nc, m.Reply, reply)
}

func decodeEnvelope(data []byte) (*model.Envelope, error) {
    var env model.Envelope
    if err := json.Unmarshal(data, &env); err != nil {
        return nil, err
    }
    return &env, nil
}

func sendReply(ctx context.Context, nc *nats.Client, reply string, msg interface{}) {
    b, _ := json.Marshal(msg)
    nc.Publish(reply, b)
}

func sendError(ctx context.Context, nc *nats.Client, reply string, err error) {
    e := &model.ErrorResponse{Code: "error", Message: err.Error()}
    b, _ := json.Marshal(e)
    nc.Publish(reply, b)
}

Step 4: Add Documentation Using Flowbite

Directory Structure

  • docs/index.md
  • docs/api.md
  • docs/installation.md

File: docs/index.md

---
layout: 'layouts/base.njk'
title: Echo Service Documentation
---

{% from "partials/nav.njk" import nav %}
{% set current = "home" %}

<div class="container mx-auto py-8">
    <h1 class="text-3xl font-bold mb-6">Welcome to Echo Service</h1>
    {{ nav(current) }}
</div>

File: docs/api.md

---
layout: 'layouts/base.njk'
title: API Reference
parent: Documentation
---

# API Reference

This section describes how to interact with the Echo Service via NATS.

## Subjects

### Commands
- Base: `cmd.<aggregate>.<action>`
- Examples:
  - `cmd.user.create`
  - `cmd.user.update`

### Queries
- Base: `query.<module>.<entity>`
- Examples:
  - `query.user.profile`
  - `query.user.list`

File: docs/installation.md

---
layout: 'layouts/base.njk'
title: Installation
parent: Documentation
---

# Installation Guide

To run the Echo Service locally:

1. Install Go.
2. Clone repository.
3. Run: `make start`.

Final Steps

  1. Run Tests: Ensure unit and integration tests pass.
  2. Start NATS Server: Make sure NATS is running locally or use Docker.
  3. Run Service: Execute go run main.go.
  4. Generate Docs: Run templ templating engine to generate HTML files.
  5. Serve Docs: Use Flowbite CLI or HTTP server to serve generated docs.

Enjoy building your idiomatic NATS-driven microservice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment