Skip to content

Commit fd53b02

Browse files
authored
refactor: extract shared BasePartyClient to reduce duplicate gRPC connection code (#400)
* feat(clients): extract shared BasePartyClient to shared/pkg/clients Introduces BasePartyClient infrastructure to reduce code duplication between tenant and current-account services when connecting to the Party service. The shared client handles common gRPC connection setup, configuration validation, timeout defaults, tracer interceptors, and context propagation (correlation ID and organization ID). * refactor(tenant): use shared BasePartyClient for Party service communication Refactor tenant service's PartyClient to embed shared BasePartyClient, removing ~75 lines of duplicate connection setup code. The tenant-specific error handling for RegisterParty (AlreadyExists, InvalidArgument, Unavailable, DeadlineExceeded) is preserved while common gRPC infrastructure is delegated to the shared package. * refactor(current-account): use shared BasePartyClient for party service communication Refactor current-account service's PartyClient to embed BasePartyClient from shared/pkg/clients, removing ~70 lines of duplicate connection setup code. Changes: - Remove local PartyClientConfig struct (use sharedclients.PartyClientConfig) - Remove local connection fields and constructor logic (delegate to BasePartyClient) - Remove local Close() method (inherited from BasePartyClient via embedding) - Update GetParty to use PrepareContext() for context preparation - Update tests to use sharedclients.PartyClientConfig and ErrPartyServiceNameRequired - Update main.go and grpc_service.go to use sharedclients.PartyClientConfig The service-specific errors (ErrPartyNotFound, ErrPartyNotActive) and business logic (ValidateParty status check) remain in the current-account package. Part of tech debt cleanup task 19 - Extract shared PartyClient. --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent 034122d commit fd53b02

9 files changed

Lines changed: 504 additions & 204 deletions

File tree

Lines changed: 23 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,136 +1,78 @@
1+
// Package clients provides gRPC client wrappers for external service communication.
12
package clients
23

34
import (
45
"context"
56
"errors"
67
"fmt"
7-
"time"
88

99
partyv1 "github.com/meridianhub/meridian/api/proto/meridian/party/v1"
1010
sharedclients "github.com/meridianhub/meridian/shared/pkg/clients"
11-
platformgrpc "github.com/meridianhub/meridian/shared/pkg/grpc"
12-
"github.com/meridianhub/meridian/shared/platform/observability"
13-
"google.golang.org/grpc"
1411
"google.golang.org/grpc/codes"
1512
"google.golang.org/grpc/status"
1613
)
1714

18-
// Party validation errors
15+
// Party validation errors.
1916
var (
20-
// ErrPartyNotFound is returned when the requested party does not exist
17+
// ErrPartyNotFound is returned when the requested party does not exist.
2118
ErrPartyNotFound = errors.New("party not found")
22-
// ErrPartyNotActive is returned when the party exists but is not in ACTIVE status
19+
// ErrPartyNotActive is returned when the party exists but is not in ACTIVE status.
2320
ErrPartyNotActive = errors.New("party not active")
24-
// ErrPartyServiceNameRequired is returned when ServiceName is not provided
25-
ErrPartyServiceNameRequired = errors.New("ServiceName is required for party client")
2621
)
2722

28-
// PartyClient defines the interface for communicating with the Party service
23+
// PartyClient defines the interface for communicating with the Party service.
2924
//
3025
// The Party service manages party reference data (customers, counterparties,
3126
// legal entities). CurrentAccount uses this service to validate party ownership
3227
// before account operations.
3328
type PartyClient interface {
34-
// ValidateParty checks if a party exists and is active
29+
// ValidateParty checks if a party exists and is active.
3530
//
3631
// Returns nil if the party exists and has ACTIVE status.
3732
// Returns ErrPartyNotFound if the party does not exist.
3833
// Returns ErrPartyNotActive if the party exists but is not ACTIVE.
3934
ValidateParty(ctx context.Context, partyID string) error
4035

41-
// GetParty retrieves full party details by ID
36+
// GetParty retrieves full party details by ID.
4237
//
4338
// Returns the party data if found, or an error if not found.
4439
GetParty(ctx context.Context, partyID string) (*partyv1.Party, error)
4540

46-
// Close terminates the client connection gracefully
41+
// Close terminates the client connection gracefully.
4742
Close() error
4843
}
4944

50-
// PartyGRPCClient implements PartyClient using gRPC
45+
// PartyGRPCClient implements PartyClient using gRPC.
46+
//
47+
// This client embeds the shared BasePartyClient for connection management
48+
// and adds current-account-specific error handling for party validation operations.
5149
type PartyGRPCClient struct {
52-
conn *grpc.ClientConn
53-
client partyv1.PartyServiceClient
54-
tracer *observability.Tracer
55-
timeout time.Duration
56-
}
57-
58-
// PartyClientConfig holds configuration for the Party client
59-
type PartyClientConfig struct {
60-
// ServiceName is the Kubernetes service name (e.g., "party").
61-
// Required. Enables DNS-based client-side load balancing via pkg/platform/grpc.
62-
// The client will connect to dns:///party.<namespace>.svc.cluster.local:<port>
63-
// and use round_robin load balancing across all pod IPs.
64-
ServiceName string
65-
66-
// Namespace is the Kubernetes namespace (e.g., "default", "production")
67-
// Defaults to "default" if not specified or empty.
68-
Namespace string
69-
70-
// Port is the service port number
71-
// Party service uses port 50055 (configured in services/party/k8s/service.yaml)
72-
Port int
73-
74-
// Timeout is the default timeout for RPC calls
75-
// If not specified, defaults to 30 seconds
76-
Timeout time.Duration
77-
78-
// Tracer is an optional observability tracer for distributed tracing
79-
// If provided, the client will automatically propagate trace context
80-
Tracer *observability.Tracer
81-
82-
// DialOptions allows custom gRPC dial options
83-
DialOptions []grpc.DialOption
50+
*sharedclients.BasePartyClient
8451
}
8552

8653
// NewPartyClient creates a new Party gRPC client using DNS-based load balancing.
8754
//
8855
// Example:
8956
//
90-
// config := &clients.PartyClientConfig{
57+
// config := &sharedclients.PartyClientConfig{
9158
// ServiceName: "party",
9259
// Namespace: "default",
9360
// Port: 50055,
9461
// Timeout: 30 * time.Second,
95-
// Tracer: tracer,
9662
// }
97-
func NewPartyClient(cfg *PartyClientConfig) (*PartyGRPCClient, error) {
98-
if cfg.ServiceName == "" {
99-
return nil, ErrPartyServiceNameRequired
100-
}
101-
102-
if cfg.Timeout == 0 {
103-
cfg.Timeout = 30 * time.Second
104-
}
105-
106-
dialOpts := cfg.DialOptions
107-
108-
if cfg.Tracer != nil {
109-
dialOpts = append(dialOpts,
110-
grpc.WithUnaryInterceptor(cfg.Tracer.UnaryClientInterceptor()),
111-
grpc.WithStreamInterceptor(cfg.Tracer.StreamClientInterceptor()),
112-
)
113-
}
114-
115-
conn, err := platformgrpc.NewClient(context.Background(), platformgrpc.ClientConfig{
116-
ServiceName: cfg.ServiceName,
117-
Namespace: cfg.Namespace,
118-
Port: cfg.Port,
119-
DialOptions: dialOpts,
120-
})
63+
// client, err := clients.NewPartyClient(config)
64+
func NewPartyClient(cfg *sharedclients.PartyClientConfig) (*PartyGRPCClient, error) {
65+
base, err := sharedclients.NewBasePartyClient(cfg)
12166
if err != nil {
122-
return nil, fmt.Errorf("failed to create gRPC connection: %w", err)
67+
return nil, err
12368
}
12469

12570
return &PartyGRPCClient{
126-
conn: conn,
127-
client: partyv1.NewPartyServiceClient(conn),
128-
tracer: cfg.Tracer,
129-
timeout: cfg.Timeout,
71+
BasePartyClient: base,
13072
}, nil
13173
}
13274

133-
// ValidateParty checks if a party exists and is active
75+
// ValidateParty checks if a party exists and is active.
13476
func (c *PartyGRPCClient) ValidateParty(ctx context.Context, partyID string) error {
13577
party, err := c.GetParty(ctx, partyID)
13678
if err != nil {
@@ -144,15 +86,12 @@ func (c *PartyGRPCClient) ValidateParty(ctx context.Context, partyID string) err
14486
return nil
14587
}
14688

147-
// GetParty retrieves full party details by ID
89+
// GetParty retrieves full party details by ID.
14890
func (c *PartyGRPCClient) GetParty(ctx context.Context, partyID string) (*partyv1.Party, error) {
149-
ctx, cancel := sharedclients.WithTimeout(ctx, c.timeout)
91+
ctx, cancel := c.PrepareContext(ctx)
15092
defer cancel()
15193

152-
ctx = sharedclients.PropagateCorrelationID(ctx)
153-
ctx = sharedclients.PropagateOrganization(ctx)
154-
155-
resp, err := c.client.RetrieveParty(ctx, &partyv1.RetrievePartyRequest{
94+
resp, err := c.Client().RetrieveParty(ctx, &partyv1.RetrievePartyRequest{
15695
PartyId: partyID,
15796
})
15897
if err != nil {
@@ -165,13 +104,3 @@ func (c *PartyGRPCClient) GetParty(ctx context.Context, partyID string) (*partyv
165104

166105
return resp.Party, nil
167106
}
168-
169-
// Close terminates the gRPC connection
170-
func (c *PartyGRPCClient) Close() error {
171-
if c.conn != nil {
172-
if err := c.conn.Close(); err != nil {
173-
return fmt.Errorf("failed to close party client connection: %w", err)
174-
}
175-
}
176-
return nil
177-
}

services/current-account/clients/party_client_test.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
"github.com/meridianhub/meridian/services/current-account/clients"
8+
sharedclients "github.com/meridianhub/meridian/shared/pkg/clients"
89
"github.com/meridianhub/meridian/shared/platform/observability"
910
"github.com/stretchr/testify/assert"
1011
"github.com/stretchr/testify/require"
@@ -14,23 +15,23 @@ import (
1415
func TestNewPartyClient_RequiresServiceName(t *testing.T) {
1516
t.Parallel()
1617

17-
cfg := &clients.PartyClientConfig{
18+
cfg := &sharedclients.PartyClientConfig{
1819
Namespace: "default",
1920
Port: 50055,
2021
Timeout: 10 * time.Second,
2122
}
2223

2324
client, err := clients.NewPartyClient(cfg)
2425

25-
assert.ErrorIs(t, err, clients.ErrPartyServiceNameRequired)
26+
assert.ErrorIs(t, err, sharedclients.ErrPartyServiceNameRequired)
2627
assert.Nil(t, client)
2728
}
2829

2930
// TestNewPartyClient_Success verifies client creation with valid configuration
3031
func TestNewPartyClient_Success(t *testing.T) {
3132
t.Parallel()
3233

33-
cfg := &clients.PartyClientConfig{
34+
cfg := &sharedclients.PartyClientConfig{
3435
ServiceName: "party",
3536
Namespace: "default",
3637
Port: 50055,
@@ -48,7 +49,7 @@ func TestNewPartyClient_Success(t *testing.T) {
4849
func TestNewPartyClient_DefaultTimeout(t *testing.T) {
4950
t.Parallel()
5051

51-
cfg := &clients.PartyClientConfig{
52+
cfg := &sharedclients.PartyClientConfig{
5253
ServiceName: "party",
5354
Namespace: "default",
5455
Port: 50055,
@@ -67,7 +68,7 @@ func TestNewPartyClient_CustomTimeout(t *testing.T) {
6768
t.Parallel()
6869

6970
customTimeout := 5 * time.Minute
70-
cfg := &clients.PartyClientConfig{
71+
cfg := &sharedclients.PartyClientConfig{
7172
ServiceName: "party",
7273
Namespace: "default",
7374
Port: 50055,
@@ -86,7 +87,7 @@ func TestNewPartyClient_WithTracer(t *testing.T) {
8687
t.Parallel()
8788

8889
tracer := &observability.Tracer{}
89-
cfg := &clients.PartyClientConfig{
90+
cfg := &sharedclients.PartyClientConfig{
9091
ServiceName: "party",
9192
Namespace: "default",
9293
Port: 50055,
@@ -105,7 +106,7 @@ func TestNewPartyClient_WithTracer(t *testing.T) {
105106
func TestNewPartyClient_DefaultNamespace(t *testing.T) {
106107
t.Parallel()
107108

108-
cfg := &clients.PartyClientConfig{
109+
cfg := &sharedclients.PartyClientConfig{
109110
ServiceName: "party",
110111
Namespace: "", // Should default to "default"
111112
Port: 50055,
@@ -123,7 +124,7 @@ func TestNewPartyClient_DefaultNamespace(t *testing.T) {
123124
func TestNewPartyClient_CustomNamespace(t *testing.T) {
124125
t.Parallel()
125126

126-
cfg := &clients.PartyClientConfig{
127+
cfg := &sharedclients.PartyClientConfig{
127128
ServiceName: "party",
128129
Namespace: "production",
129130
Port: 50055,
@@ -141,7 +142,7 @@ func TestNewPartyClient_CustomNamespace(t *testing.T) {
141142
func TestPartyClient_Close_Success(t *testing.T) {
142143
t.Parallel()
143144

144-
cfg := &clients.PartyClientConfig{
145+
cfg := &sharedclients.PartyClientConfig{
145146
ServiceName: "party",
146147
Namespace: "default",
147148
Port: 50055,
@@ -161,7 +162,7 @@ func TestPartyClient_Close_Success(t *testing.T) {
161162
func TestPartyClient_Close_Multiple(t *testing.T) {
162163
t.Parallel()
163164

164-
cfg := &clients.PartyClientConfig{
165+
cfg := &sharedclients.PartyClientConfig{
165166
ServiceName: "party",
166167
Namespace: "default",
167168
Port: 50055,
@@ -185,7 +186,7 @@ func TestPartyClient_Close_Multiple(t *testing.T) {
185186
func TestNewPartyClient_NilTracer(t *testing.T) {
186187
t.Parallel()
187188

188-
cfg := &clients.PartyClientConfig{
189+
cfg := &sharedclients.PartyClientConfig{
189190
ServiceName: "party",
190191
Namespace: "default",
191192
Port: 50055,

services/current-account/cmd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ func createServiceWithClients(
441441
)
442442

443443
// Create Party client with DNS-based load balancing
444-
partyGRPCClient, err := clients.NewPartyClient(&clients.PartyClientConfig{
444+
partyGRPCClient, err := clients.NewPartyClient(&sharedclients.PartyClientConfig{
445445
ServiceName: "party",
446446
Namespace: namespace,
447447
Port: 50055, // Party service port (see services/party/k8s/service.yaml)

services/current-account/service/grpc_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ func NewServiceWithClients(config Config) (*Service, error) {
228228
// Create Party client (optional - nil client provides backward compatibility)
229229
var resilientPartyClient clients.PartyClient
230230
if config.PartyServiceName != "" {
231-
partyGRPCClient, err := clients.NewPartyClient(&clients.PartyClientConfig{
231+
partyGRPCClient, err := clients.NewPartyClient(&sharedclients.PartyClientConfig{
232232
ServiceName: config.PartyServiceName,
233233
Namespace: config.Namespace,
234234
Port: config.PartyPort,

0 commit comments

Comments
 (0)