Modern mobile applications require powerful and scalable backend services. At Chipode, we chose microservice architecture and gRPC technology to manage game data and optimize the user experience. In this article, we will explain in detail why we chose these technologies and how we implemented them.
What is Microservice Architecture?
Microservice architecture is an approach to managing large and complex applications by dividing them into smaller, independent services. Each service:
– Can have its own database
– Can be deployed independently
– Can be written in different programming languages
– Horizontally scalable
1. Why Microservice?
Reasons for choosing microservice architecture for our Sudoku application:
1. Independent scaling (e.g. score service)
2. Technology flexibility
3. Fault isolation
4. Easy maintenance
5. Team independence
What is gRPC and why should we use it?
gRPC is a modern RPC (Remote Procedure Call) framework developed by Google. Highlights:
– High performance with Protocol Buffers
– Bidirectional streaming
– Language agnostic structure
– Auto-generated client/server code
– HTTP/2 based communication
REST vs gRPC Comparison
Service Identification with Protocol Buffers
protobuf
syntax = "proto3";
package game;
service GameService {
// Game save
rpc SaveGame (SaveGameRequest) returns (SaveGameResponse);
// Leaderboard streaming
rpc GetLeaderboard (LeaderboardRequest) returns (stream LeaderboardEntry);
// User statistics
rpc GetUserStats (UserStatsRequest) returns (UserStatsResponse);
// Real-time game state
rpc StreamGameState (stream GameStateUpdate) returns (stream GameStateResponse);
}
message SaveGameRequest {
string user_id = 1;
int32 score = 2;
int32 time_spent = 3;
repeated int32 board_state = 4;
GameDifficulty difficulty = 5;
}
enum GameDifficulty {
EASY = 0;
MEDIUM = 1;
HARD = 2;
EXPERT = 3;
}
// Other message definitions...
gRPC Server Implementation with Golang
1. Server Structure
go
type gameServer struct {
pb.UnimplementedGameServiceServer
db *sql.DB
redis *redis.Client
validator *validation.Validator
}
func NewGameServer(db *sql.DB, redis *redis.Client) *gameServer {
return &gameServer{
db: db,
redis: redis,
validator: validation.New(),
}
}
2. Game Saving Process
go
func (s *gameServer) SaveGame(ctx context.Context, req *pb.SaveGameRequest) (*pb.SaveGameResponse, error) {
// Validation
if err := s.validator.Struct(req); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid request: %v", err)
}
// Transaction start
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, status.Errorf(codes.Internal, "transaction error: %v", err)
}
defer tx.Rollback()
// Save game data
result, err := tx.ExecContext(ctx,
`INSERT INTO games (user_id, score, time_spent, board_state, difficulty)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`,
req.UserId, req.Score, req.TimeSpent,
pq.Array(req.BoardState), req.Difficulty,
)
if err != nil {
return nil, status.Errorf(codes.Internal, "db error: %v", err)
}
// Update leaderboard
err = s.updateLeaderboard(ctx, req.UserId, req.Score, req.Difficulty)
if err != nil {
return nil, status.Errorf(codes.Internal, "leaderboard error: %v", err)
}
// Commit the transaction
if err := tx.Commit(); err != nil {
return nil, status.Errorf(codes.Internal, "commit error: %v", err)
}
return &pb.SaveGameResponse{Success: true}, nil
}
3. Leaderboard with Redis
go
func (s *gameServer) updateLeaderboard(ctx context.Context, userId string, score int32, difficulty pb.GameDifficulty) error {
// Leaderboard key by difficulty level
leaderboardKey := fmt.Sprintf("leaderboard:%s", difficulty.String())
// Score update using Z-Set
member := &redis.Z{
Score: float64(score),
Member: userId,
}
pipe := s.redis.Pipeline()
pipe.ZAdd(ctx, leaderboardKey, member)
pipe.ZRemRangeByRank(ctx, leaderboardKey, 0, -101) // Sadece top 100
_, err := pipe.Exec(ctx)
return err
}
gRPC Client Integration with Flutter
1. gRPC Channel Configuration
dart
class GrpcClient {
static GameServiceClient? _client;
static Future get client async {
if (_client != null) return _client!;
final channel = ClientChannel(
'api.chipode.com',
port: 50051,
options: ChannelOptions(
credentials: await _getCredentials(),
idleTimeout: Duration(minutes: 1),
),
);
_client = GameServiceClient(channel);
return _client!;
}
static Future _getCredentials() async {
final trustStore = await rootBundle.load('assets/certificates/ca.pem');
return ChannelCredentials.secure(
certificates: trustStore.buffer.asUint8List(),
);
}
}
2. Game Repository Implementation
dart
class GameRepository {
final GameServiceClient _client;
GameRepository(this._client);
Future saveGame(GameState state) async {
try {
final request = SaveGameRequest()
..userId = getCurrentUserId()
..score = state.score
..timeSpent = state.timeSpent
..boardState.addAll(state.board.expand((row) => row))
..difficulty = _difficultyToProto(state.difficulty);
final response = await _client.saveGame(request);
return response.success;
} catch (e) {
log.error('Save game error', e);
rethrow;
}
}
Stream getLeaderboard(GameDifficulty difficulty) {
final request = LeaderboardRequest()
..difficulty = _difficultyToProto(difficulty);
return _client.getLeaderboard(request);
}
}
Error Handling and Retry Mechanism
1. Use of Interceptor
dart
class RetryInterceptor extends ClientInterceptor {
@override
ResponseFuture interceptUnary(
ClientMethod method,
Q request,
CallOptions options,
ClientUnaryInvoker invoker,
) {
return _retry(
() => invoker(method, request, options),
maxAttempts: 3,
);
}
Future _retry(
Future Function() call, {
int maxAttempts = 3,
}) async {
int attempts = 0;
while (true) {
try {
attempts++;
return await call();
} catch (e) {
if (attempts >= maxAttempts) rethrow;
await Future.delayed(Duration(seconds: attempts));
}
}
}
}
Monitoring and Logging
1. Prometheus Metrics
go
var (
requestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "grpc_request_duration_seconds",
Help: "gRPC request duration in seconds",
},
[]string{"method", "status"},
)
activeConnections = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "grpc_active_connections",
Help: "Number of active gRPC connections",
},
)
)
2. Structured Logging
go
func (s *gameServer) SaveGame(ctx context.Context, req *pb.SaveGameRequest) (*pb.SaveGameResponse, error) {
logger := log.With().
Str("user_id", req.UserId).
Int32("score", req.Score).
Str("difficulty", req.Difficulty.String()).
Logger()
logger.Info().Msg("saving game")
// ... implementation
}
Result
gRPC and microservices architecture underpin Chipode’s backend infrastructure. Thanks to these technologies:
1. High performance communication
2. Easy scaling
Type 3 safety
4. Effective monitoring
5. Reliable error handling
to optimize the infrastructure. In future articles, we will examine the other components of this infrastructure and optimization techniques in detail.





