Learnitweb

Generating Separate Java Classes from Protobuf Messages with java_multiple_files

1. Introduction

When working with Protobuf in Java projects, one common surprise is the “outer class” that Protobuf generates around your messages. Many developers expect to access their message types directly, but instead they find themselves navigating through a wrapper class.

This tutorial explains:

  • What the outer class is
  • Why Protobuf generates it
  • How to remove the need to use it
  • How java_multiple_files works
  • How to use the generated classes in Java

We will build a clear mental model so you understand what Protobuf is doing behind the scenes and how to configure it for cleaner Java usage.

2. The Default Behavior: Outer Class Generation

By default, when you define a .proto file like this:

syntax = "proto3";

package section02;

option java_package = "com.learnitweb.models.section02";

message Person {
  string name = 1;
  int32 age = 2;
}

Protobuf typically generates:

  • One outer Java class named after the proto file
  • Inner classes inside it for each message

If your file is named:

Person.proto

Protobuf may generate something like:

PersonOuterClass.Person

This means your actual message type is nested inside another class.

So instead of:

Person.newBuilder()

you might need:

PersonOuterClass.Person.newBuilder()

This feels verbose and inconvenient in real projects.

3. The Goal

We want to access the message directly like a normal Java class.

Example:

Person.newBuilder()

without going through an outer wrapper.

4. The Solution: java_multiple_files

Protobuf provides a Java-specific option:

option java_multiple_files = true;

By default:

java_multiple_files = false

When set to true, Protobuf generates:

  • One Java file per message
  • One Java file per enum
  • One Java file per service

Instead of grouping everything inside one outer class.

5. Updated Proto Configuration

Here is the improved .proto:

syntax = "proto3";

package section02;

option java_package = "com.learnitweb.models.section02";
option java_multiple_files = true;

message Person {
  string name = 1;
  int32 age = 2;
}

6. What Happens After mvn clean compile

When you run:

mvn clean compile

Now Protobuf generates:

Person.java
PersonOrBuilder.java
<ProtoFileName>.java (descriptor holder)

Important Understanding

The Person.java is now your actual message class. You can use it directly.

Example:

Person person = Person.newBuilder()
    .setName("Sam")
    .setAge(12)
    .build();

This is much cleaner and matches normal Java expectations.

7. But Why Do We Still See Another Class?

Even with java_multiple_files = true, you may still see a class related to the proto file itself.

Example:

Hello.java

if your file is named:

hello.proto

What is this class?

This class represents metadata about the entire proto file.

It contains:

  • Descriptor information
  • Schema metadata
  • Internal references used by Protobuf runtime

This is not meant for everyday use.

You usually do not touch this class directly.

8. Why the “Outer Class” Appears Sometimes

If your proto file and message share the same name:

Person.proto
message Person

Protobuf avoids naming conflicts by introducing a wrapper/outer structure.

This is a naming conflict resolution strategy.

9. Simple Naming Strategy to Avoid Confusion

A practical tip:

  • Avoid giving proto files the exact same name as messages
  • Use descriptive file names

Example:

person_messages.proto
user_models.proto
account_types.proto

This reduces confusion and keeps generated code predictable.

10. Using the Generated Class in Java

Example main class:

package com.guru.section02;

import com.guru.models.section02.Person;

public class MainApp {
    public static void main(String[] args) {
        Person person = Person.newBuilder()
                .setName("Sam")
                .setAge(12)
                .build();

        System.out.println(person);
    }
}

What Happens Here

The builder pattern is automatically generated by Protobuf, which allows safe and structured object creation.

You do not implement the builder yourself because Protobuf generates the implementation behind the scenes.

The message class is final because Protobuf messages are immutable.

11. Best Practices for Java + Protobuf

Always enable java_multiple_files = true in Java projects because it produces cleaner and more modular code.

Always define java_package explicitly to keep generated code organized.

Treat descriptor/outer classes as internal infrastructure rather than application-level code.

Use meaningful proto file names that do not clash with message names.

Rely on builders to create message instances because Protobuf messages are immutable by design.