Echo Service
The Echo Service is a simple microservice that receives any data payload via NATS messaging and returns the exact same data payload in its response.
-
NATS Integration
- The service must connect to a NATS server.
- It must subscribe to a specific subject (e.g.,
echo.request). - Upon receiving a message, it must publish the same message back to a reply subject provided by the client (e.g.,
client.reply.subject).
-
Response Format
- The service must return the received data unchanged.
- If the incoming message has a header (
nats.Header), the response must include the same headers.
-
Error Handling
- If an error occurs during processing or publishing, the service should log the error but not modify the original message content.
-
Performance
- The service should handle multiple concurrent requests efficiently.
- Latency should be minimized without compromising correctness.
-
Documentation
- Provide usage documentation for connecting clients and expected behavior.
- Language/Framework: Go
- Testing Framework: Go’s standard testing package + table-driven tests.
- Dependency Management: Use Go modules.
- Logging: Basic stdlib logging or use a minimal wrapper around it.
- Code Quality: Follow clean architecture principles; prefer interfaces over concrete types where possible.
Define a core port interface that specifies the business logic contract.
package echo
type ServicePort interface {
Echo(ctx context.Context, msg *nats.Msg) error
}Implement the core port using application services.
package echo
import (
"context"
"log"
)
type AppService struct {
port ServicePort
}
func NewAppService(port ServicePort) *AppService {
return &AppService{port: port}
}
func (s *AppService) HandleRequest(ctx context.Context, msg *nats.Msg) error {
return s.port.Echo(ctx, msg)
}Provide the infrastructure implementation of the port, which includes NATS integration.
package echo
import (
"context"
"fmt"
"github.com/nats-io/nats.go"
)
type NATSPort struct {
nc *nats.Conn
}
func NewNATSPort(nc *nats.Conn) *NATSPort {
return &NATSPort{nc: nc}
}
func (p *NATSPort) Echo(ctx context.Context, msg *nats.Msg) error {
return p.nc.Publish(msg.Reply, msg.Data)
}First, write unit tests for the application layer.
package echo_test
import (
"context"
"testing"
echo "your-package-name/echo"
)
func TestAppService_HandleRequest(t *testing.T) {
ctx := context.Background()
msg := &nats.Msg{
Data: []byte("hello"),
Reply: "reply.subject",
}
tests := []struct {
name string
wantErr bool
}{
{"should pass when echo succeeds", false},
{"should fail if echo fails", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
portMock := &echo.ServicePortMock{
EchoFunc: func(ctx context.Context, msg *nats.Msg) error {
if tt.wantErr {
return fmt.Errorf("forced error")
}
return nil
},
}
app := echo.NewAppService(portMock)
err := app.HandleRequest(ctx, msg)
if (err != nil) != tt.wantErr {
t.Errorf("HandleRequest() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}Create a mock for the service port.
package echo
type ServicePortMock struct {
EchoFunc func(ctx context.Context, msg *nats.Msg) error
}
func (m *ServicePortMock) Echo(ctx context.Context, msg *nats.Msg) error {
return m.EchoFunc(ctx, msg)
}Next, write integration tests verifying NATS connectivity and message round-trip.
package echo_test
import (
"context"
"encoding/json"
"testing"
"github.com/nats-io/nats.go"
echo "your-package-name/echo"
natsutil "github.com/nats-io/nats.go/testutils"
)
func TestNATSPort_Echo(t *testing.T) {
s, opts := natsutil.CreateServerWithConf(nats.DefaultTestOptions)
defer s.Stop()
nc, _ := nats.Connect(opts.URI)
defer nc.Close()
port := echo.NewNATSPort(nc)
subj := "echo.request"
reply := "client.reply.subject"
request := map[string]interface{}{
"message": "test",
}
expectedJSON, _ := json.Marshal(request)
msg := &nats.Msg{
Subject: subj,
Reply: reply,
Data: expectedJSON,
}
done := make(chan error, 1)
go func() {
done <- port.Echo(context.Background(), msg)
}()
receivedMsg := <-s.InboxSub("client.reply.")
var receivedData map[string]interface{}
json.Unmarshal(receivedMsg.Data, &receivedData)
if string(receivedMsg.Data) != string(expectedJSON) {
t.Errorf("Expected %s, got %s", expectedJSON, receivedMsg.Data)
}
if err := <-done; err != nil {
t.Errorf("Unexpected error: %v", err)
}
}Bootstrap the service.
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/nats-io/nats.go"
echo "your-package-name/echo"
)
const (
subject = "echo.request"
)
func main() {
nc, err := nats.Connect(nats.DefaultURL)
if err != nil {
log.Fatalf("Failed to connect to NATS: %v", err)
}
defer nc.Close()
app := echo.NewAppService(echo.NewNATSPort(nc))
// Subscribe to messages
_, err = nc.Subscribe(subject, func(msg *nats.Msg) {
if err := app.HandleRequest(context.Background(), msg); err != nil {
log.Printf("Error handling request: %v", err)
}
})
if err != nil {
log.Fatalf("Failed to subscribe: %v", err)
}
log.Println("Echo service started")
// Graceful shutdown
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
sig := <-signals
log.Printf("Received signal: %s, shutting down...", sig)
}As per the ports & adapters design above, no additional changes are needed here.
Already covered in the ports & adapters section.
Already covered in the ports & adapters section.
This completes the specification, design, TDD approach, and actual code implementation for the Echo Service using NATS, CQRS principles, Flowbite styling hints, and idiomatic Go practices.