Harmonize ALL THE THINGS!

Backend engineer? Frontend Engineer? DevOps, oh wait, Platform engineer? Anyway: if you’ve spent more than five minutes building a modern distributed system, you’ve lived this meme. You start with a grand vision of a unified architecture, but by lunchtime, you’re screaming into a Chat channel because a field named UserID in your Go code somehow became user_id in your stored marshalled object store, and is now appearing as userId in your React frontend.

It’s the classic “Harmonize All The Things” paradox. We want consistency, but we’re fighting a multi-front war of naming conventions….

The Great Naming Divide

As a software engineer, I’m constantly playing with these three distinct “cultures.” They all want the same thing, which is clarity, but they speak different dialects.

When you look at a typical stack involving Go, Protobuf, and Frontend JSON, you aren’t just writing code; you’re a translator. Here is how the “consistency” breaks down:

  • Protobuf (The definition of the interfaces): Per the Google style guide, we use snake_case. It’s language-neutral and looks clean in a .proto file.
  • Golang (The Engine): Go is opinionated. Capitalization equals visibility. lowerCasing is a private field, If you want a field to be public, it must be PascalCase.
  • The Frontend (The API Consumer): Your JavaScript/TypeScript devs will look at you like you have two heads if you send them a mixture of the above and put some legacy snake_case, PascalCase. In the land of the browser, JSON keys are expected and camelCase is king.

Let’s dive into these in a bit more detail…

Protobuf uses snake_case

As per the style guide of protobuf, the Field names should be snake_cased, for example:


package music.v1

syntax = "proto3"

message Album {
  string name = 1;
}

message AlbumsResponse {
   string author_name = 1; // The name of the author or band
   repeated Album album = 2; // A list of albums
}

So, as you can see, the author_name field is snake_cased. So is the album name as it also starts with lowercase.

Generated golang code from proto becomes PascalCase

Now when we generate golang code from the above, using a tool like protoc-gen-go, we get this:

// Code generated by protoc-gen-go. DO NOT EDIT.
package v1

import (
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
)

type Album struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	// Proto: string name = 1;
	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}

type AlbumsResponse struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	// Proto: string author_name = 1;
	AuthorName string   `protobuf:"bytes,1,opt,name=author_name,json=authorName,proto3" json:"author_name,omitempty"`
	// Proto: repeated Album albums = 2;
	Albums     []*Album `protobuf:"bytes,2,rep,name=albums,proto3" json:"albums,omitempty"`
}

Note that apart from the necessary protobuf serialization private fields, state, sizeCache, unknownFields (all camelCased, thus private in golang) the protobuf field names AuthorName and Albums, suddenly became PascalCase, starting with an uppercase as first character! This is due to golangs’ internal behaviour where only the capitalized fieldnames are publicly available and accessible when you instantiate a variable with these struct types. But now, as you can see in the generated code, there are multiple json versions when these structs get marshalled. Turns out that in golang you can use protojson.Marshal, which will use the industry standard camelCasing, as per the Google JSON Style Guide, while when you use json.Marshal, the legacy author_name snake_case field name is used, which is simply copying over the field name as was used in the protobuf.

The best way is to simply use protojson.Marshal when marshalling protoc generated golang structs.

What does the Frontend / GUI get from a gRPC call for this protobuf then?

Depends. When the frontend used gRPC-Web, the frontend does not really see camelCase or snake_case, but has to refer to the communication defined proto file, as shown earlier. Then when defining the frontend code, using protoc-gen-js or protoc-gen-ts (for TypeScript) the Frontend gets the necessary accessor for the fields. So for a proto field of string author_name = 1; the frontend code will have const name = message.getAuthorName(); or message.authorName. Both nicely in the Google JSON Style Guides camelCase version.

In case the gRPC server uses grpc-gateway, then the server internally already uses the protojson marshaller to generate JSON respones. The code in the Frontend will then also receive the fields camelCased:

{
  "authorName": "The Beatles",
  "albums": [...]
}

So, either way, the Frontend will see camelCase.

Well ehm… Why is this so hard?

The truth is, even with the best compilers, the “vibe” and “standards” of our code shifts as it travels through the stack. The protoc compiler does its best to map user_name to UserName, but the moment that data hits a JSON marshal, the “Harmonize” part of the meme becomes a nightmare.

Why “Harmonization” is the silent killer of productivity:

  • Mapping Drift: You change a field name in a .proto file, to better catch its’ meaning and suddenly your frontend breaks because the JSON tag didn’t auto-update.
  • Manual Boilerplate: We spend 20% of our time writing business logic and 80% writing CRUD code and struct tags like `json:"userId"` just to make the layers talk to each other.
  • Human Reasoning Gaps: When a new dev joins the team, they see three different names for the same variable. The rationale is “standardization,” but the result is a cognitive tax every time they read different parts of the code to get an understanding of the full architecture.

In Short: We are the Curators of Chaos

Just like I mentioned in my post about Vibe Coding, the role of the developer is evolving. We aren’t just “coding” any more; we are managing the friction between these different layers. And the irony, The Irony, the_irony: It is the ultimate “developer move”: we have a tool (protoc) that generates golang code from a protobuf schema, nicely put aside in a myproto_def.pb.go file, so we need another tool (protoc-gen-go-json) to generate more code myproto_def.pb.json.go to fix the way the first code handles JSON. We use tools like protoc-gen-go-json or custom templates to automate the “harmonizing,” but the human oversight remains the most critical part.

Unless we are careful, our “consistent systems” become a tangled web of snake_case, camelCase, and PascalCase that no LLM can fully untangle for us or the poor consumers of our data.

Developing, mapping, refactoring. It’s all part of the job. But I have to ask: do we actually want one case to rule them all, or is the friction where the real engineering happens?

What do you think? Are you a strict snake_case purist, or have you embraced the chaos of the mapping layers? Feel free to leave a comment below!


References

Follow this blog on Mastodon or the Fediverse to receive updates directly in your feed.

Melle
Melle
28 posts
1 follower
Fediverse Reactions