You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ignite.apache.org by GitBox <gi...@apache.org> on 2022/03/14 19:35:51 UTC

[GitHub] [ignite-3] sashapolo commented on a change in pull request #714: IGNITE-16674 Fix incremental compilation for network messages

sashapolo commented on a change in pull request #714:
URL: https://github.com/apache/ignite-3/pull/714#discussion_r825770692



##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.
+ */
+class IncrementalCompilationConfig {
+    /** Incremental compilation configuration file name. */
+    static final String CONFIG_FILE_NAME = "META-INF/transferable.messages";
+
+    /** Message group class name. */
+    private ClassName messageGroupClassName;
+
+    /** Messages. */
+    private final List<ClassName> messageClasses;
+
+    IncrementalCompilationConfig(ClassName messageGroupClassName, List<ClassName> messageClasses) {
+        this.messageGroupClassName = messageGroupClassName;
+        this.messageClasses = messageClasses;
+    }
+
+    /**
+     * Saves configuration on disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    void writeConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject fileObject;
+        try {
+            fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+
+        try (OutputStream out = fileObject.openOutputStream()) {
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+            writeClassName(writer, messageGroupClassName);
+            writer.newLine();
+
+            for (ClassName messageClassName : messageClasses) {
+                writeClassName(writer, messageClassName);
+                writer.newLine();
+            }
+
+            writer.flush();
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+    }
+
+    /**
+     * Reads configuration from disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    static IncrementalCompilationConfig readConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject resource;
+
+        try {
+            resource = filer.getResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            return null;
+        }
+
+        try (Reader reader = resource.openReader(true)) {
+            BufferedReader bufferedReader = new BufferedReader(reader);
+            String messageClassNameString = bufferedReader.readLine();
+
+            if (messageClassNameString == null) {
+                return null;
+            }
+
+            ClassName messageClassName = readClassName(messageClassNameString);
+
+            List<ClassName> message = new ArrayList<>();
+
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {

Review comment:
       can be replaced with `bufferedReader.lines().map(IncrementalCompilationConfig::readClassName).collect(toList())`

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.
+ */
+class IncrementalCompilationConfig {
+    /** Incremental compilation configuration file name. */
+    static final String CONFIG_FILE_NAME = "META-INF/transferable.messages";
+
+    /** Message group class name. */
+    private ClassName messageGroupClassName;
+
+    /** Messages. */
+    private final List<ClassName> messageClasses;
+
+    IncrementalCompilationConfig(ClassName messageGroupClassName, List<ClassName> messageClasses) {
+        this.messageGroupClassName = messageGroupClassName;
+        this.messageClasses = messageClasses;
+    }
+
+    /**
+     * Saves configuration on disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    void writeConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject fileObject;
+        try {
+            fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+
+        try (OutputStream out = fileObject.openOutputStream()) {
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+            writeClassName(writer, messageGroupClassName);
+            writer.newLine();
+
+            for (ClassName messageClassName : messageClasses) {
+                writeClassName(writer, messageClassName);
+                writer.newLine();
+            }
+
+            writer.flush();
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+    }
+
+    /**
+     * Reads configuration from disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    static IncrementalCompilationConfig readConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject resource;
+
+        try {
+            resource = filer.getResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            return null;
+        }
+
+        try (Reader reader = resource.openReader(true)) {
+            BufferedReader bufferedReader = new BufferedReader(reader);

Review comment:
       please move the `bufferedReader` inside the `try` block, IDEA complains that it is not closed

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.
+ */
+class IncrementalCompilationConfig {
+    /** Incremental compilation configuration file name. */
+    static final String CONFIG_FILE_NAME = "META-INF/transferable.messages";
+
+    /** Message group class name. */
+    private ClassName messageGroupClassName;
+
+    /** Messages. */
+    private final List<ClassName> messageClasses;
+
+    IncrementalCompilationConfig(ClassName messageGroupClassName, List<ClassName> messageClasses) {
+        this.messageGroupClassName = messageGroupClassName;
+        this.messageClasses = messageClasses;
+    }
+
+    /**
+     * Saves configuration on disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    void writeConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject fileObject;
+        try {
+            fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+
+        try (OutputStream out = fileObject.openOutputStream()) {
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+            writeClassName(writer, messageGroupClassName);
+            writer.newLine();
+
+            for (ClassName messageClassName : messageClasses) {
+                writeClassName(writer, messageClassName);
+                writer.newLine();
+            }
+
+            writer.flush();
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+    }
+
+    /**
+     * Reads configuration from disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    static IncrementalCompilationConfig readConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject resource;
+
+        try {
+            resource = filer.getResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            return null;
+        }
+
+        try (Reader reader = resource.openReader(true)) {
+            BufferedReader bufferedReader = new BufferedReader(reader);
+            String messageClassNameString = bufferedReader.readLine();
+
+            if (messageClassNameString == null) {
+                return null;
+            }
+
+            ClassName messageClassName = readClassName(messageClassNameString);
+
+            List<ClassName> message = new ArrayList<>();
+
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                ClassName className = readClassName(line);
+                message.add(className);
+            }
+
+            return new IncrementalCompilationConfig(messageClassName, message);
+        } catch (FileNotFoundException | NoSuchFileException e) {
+            return null;
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+    }
+
+    /**
+     * Writes class name with all the enclosing classes.
+     *
+     * @param writer Writer.
+     * @param className Class name.
+     * @throws IOException If failed.
+     */
+    private void writeClassName(BufferedWriter writer, ClassName className) throws IOException {
+        writer.write(className.packageName());
+        writer.write(' ');
+
+        List<String> enclosingSimpleNames = new ArrayList<>();
+        ClassName enclosing = className;
+        while ((enclosing = enclosing.enclosingClassName()) != null) {
+            enclosingSimpleNames.add(enclosing.simpleName());
+        }
+        Collections.reverse(enclosingSimpleNames);
+        for (String enclosingSimpleName : enclosingSimpleNames) {
+            writer.write(enclosingSimpleName);
+            writer.write(' ');
+        }
+
+        writer.write(className.simpleName());
+    }
+
+    /**
+     * Reads class name.
+     *
+     * @param line Line.
+     * @return Class name.
+     */
+    static ClassName readClassName(String line) {
+        String[] split = line.split(" ");
+        String packageName = split[0];
+        String simpleName = split[1];
+        String[] simpleNames = {};

Review comment:
       can be written shorter: `String[] simpleNames = split.length > 2 ? Arrays.copyOfRange(split, 2, split.length) : new String[0]`

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.
+ */
+class IncrementalCompilationConfig {
+    /** Incremental compilation configuration file name. */
+    static final String CONFIG_FILE_NAME = "META-INF/transferable.messages";
+
+    /** Message group class name. */
+    private ClassName messageGroupClassName;
+
+    /** Messages. */
+    private final List<ClassName> messageClasses;
+
+    IncrementalCompilationConfig(ClassName messageGroupClassName, List<ClassName> messageClasses) {
+        this.messageGroupClassName = messageGroupClassName;
+        this.messageClasses = messageClasses;
+    }
+
+    /**
+     * Saves configuration on disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    void writeConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject fileObject;
+        try {
+            fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+
+        try (OutputStream out = fileObject.openOutputStream()) {
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+            writeClassName(writer, messageGroupClassName);
+            writer.newLine();
+
+            for (ClassName messageClassName : messageClasses) {
+                writeClassName(writer, messageClassName);
+                writer.newLine();
+            }
+
+            writer.flush();
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+    }
+
+    /**
+     * Reads configuration from disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    static IncrementalCompilationConfig readConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject resource;
+
+        try {
+            resource = filer.getResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            return null;
+        }
+
+        try (Reader reader = resource.openReader(true)) {
+            BufferedReader bufferedReader = new BufferedReader(reader);
+            String messageClassNameString = bufferedReader.readLine();
+
+            if (messageClassNameString == null) {
+                return null;
+            }
+
+            ClassName messageClassName = readClassName(messageClassNameString);
+
+            List<ClassName> message = new ArrayList<>();
+
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                ClassName className = readClassName(line);
+                message.add(className);
+            }
+
+            return new IncrementalCompilationConfig(messageClassName, message);
+        } catch (FileNotFoundException | NoSuchFileException e) {

Review comment:
       which method throws these exceptions?

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.

Review comment:
       please add a comment about the serialized format of this config

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.
+ */
+class IncrementalCompilationConfig {
+    /** Incremental compilation configuration file name. */
+    static final String CONFIG_FILE_NAME = "META-INF/transferable.messages";
+
+    /** Message group class name. */
+    private ClassName messageGroupClassName;
+
+    /** Messages. */
+    private final List<ClassName> messageClasses;
+
+    IncrementalCompilationConfig(ClassName messageGroupClassName, List<ClassName> messageClasses) {
+        this.messageGroupClassName = messageGroupClassName;
+        this.messageClasses = messageClasses;
+    }
+
+    /**
+     * Saves configuration on disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    void writeConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject fileObject;
+        try {
+            fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+
+        try (OutputStream out = fileObject.openOutputStream()) {

Review comment:
       It's strange that you are using an `OutputStream` with a `UTF-8` encoding in the `write` method, but a `Reader` with the default charset in the `read` method. I think you should either use `fileObject.openWriter` here or the `InputStream` in the `read` method

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/MessageClass.java
##########
@@ -141,13 +141,36 @@ public String simpleName() {
         return getters;
     }
 
+    /**
+     * Returns class name that the generated SerializationFactory should have.
+     *
+     * @return Class name that the generated SerializationFactory should have.
+     */
+    public ClassName serializationFactoryName() {
+        return serializationFactoryName(className);
+    }
+
+    /**
+     * Implementation of the {@link MessageClass#serializationFactoryName()}.
+     */
+    public static ClassName serializationFactoryName(ClassName className) {

Review comment:
       All these static methods are clearly an abstraction leak. I would suggest introducing a `MessageClassNames` class, that will hold a `ClassName` and will have all these methods

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.
+ */
+class IncrementalCompilationConfig {
+    /** Incremental compilation configuration file name. */
+    static final String CONFIG_FILE_NAME = "META-INF/transferable.messages";
+
+    /** Message group class name. */
+    private ClassName messageGroupClassName;
+
+    /** Messages. */
+    private final List<ClassName> messageClasses;
+
+    IncrementalCompilationConfig(ClassName messageGroupClassName, List<ClassName> messageClasses) {
+        this.messageGroupClassName = messageGroupClassName;
+        this.messageClasses = messageClasses;
+    }
+
+    /**
+     * Saves configuration on disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    void writeConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject fileObject;
+        try {
+            fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+
+        try (OutputStream out = fileObject.openOutputStream()) {
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+            writeClassName(writer, messageGroupClassName);
+            writer.newLine();

Review comment:
       I would suggest moving the `newLine()` call inside `writeClassName` 

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/TransferableObjectProcessor.java
##########
@@ -71,24 +74,71 @@ public SourceVersion getSupportedSourceVersion() {
     @Override
     public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
         try {
+            IncrementalCompilationConfig currentConfig = IncrementalCompilationConfig.readConfig(processingEnv);
+
             List<MessageClass> messages = annotations.stream()
                     .map(roundEnv::getElementsAnnotatedWith)
                     .flatMap(Collection::stream)
                     .map(TypeElement.class::cast)
                     .map(e -> new MessageClass(processingEnv, e))
-                    .collect(Collectors.toList());
+                    .collect(toList());
 
             if (messages.isEmpty()) {
                 return true;
             }
 
-            MessageGroupWrapper messageGroup = getMessageGroup(roundEnv);
+            MessageGroupWrapper messageGroup = getMessageGroup(roundEnv, currentConfig);
+
+            if (currentConfig == null) {
+                List<ClassName> messageClassNames = messages.stream()
+                        .map(MessageClass::className)
+                        .collect(toList());
+
+                currentConfig = new IncrementalCompilationConfig(ClassName.get(messageGroup.element()), messageClassNames);
+            } else {
+                List<ClassName> messageClassesFromConfig = currentConfig.messageClasses();

Review comment:
       I can see that you nearly always convert the `ClassName`s from the config into `TypeElement`s. Maybe that should be done inside the config class

##########
File path: modules/network/src/integrationTest/java/org/apache/ignite/internal/network/processor/InMemoryJavaFileManager.java
##########
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.io.ByteSource;
+import com.google.testing.compile.ForwardingStandardJavaFileManager;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import javax.tools.FileObject;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+
+/**
+ * A file manager implementation that stores all output in memory.
+ */
+final class InMemoryJavaFileManager extends ForwardingStandardJavaFileManager {

Review comment:
       Can you please explain why do you need such a complex class here? Why does it have to implement `StandardJavaFileManager` and not `JavaFileManager`?

##########
File path: modules/network/src/integrationTest/java/org/apache/ignite/internal/network/processor/ItTransferableObjectProcessorIncrementalTest.java
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static org.apache.ignite.internal.network.processor.IncrementalCompilationConfig.CONFIG_FILE_NAME;
+import static org.apache.ignite.internal.network.processor.IncrementalCompilationConfig.readClassName;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.testing.compile.JavaFileObjects;
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+import javax.tools.DiagnosticCollector;
+import javax.tools.FileObject;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaCompiler.CompilationTask;
+import javax.tools.JavaFileManager.Location;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+import org.intellij.lang.annotations.Language;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Integration tests for the {@link TransferableObjectProcessor} incremental compilation.
+ */
+public class ItTransferableObjectProcessorIncrementalTest {
+    /**
+     * Package name of the test sources.
+     */
+    private static final String RESOURCE_PACKAGE_NAME = "org.apache.ignite.internal.network.processor";
+
+    /** File manager for incremental compilation. */
+    private InMemoryJavaFileManager fileManager;
+
+    @BeforeEach
+    void setUp() {
+        DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
+        JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler();
+        StandardJavaFileManager standardFileManager = systemJavaCompiler.getStandardFileManager(diagnosticCollector, Locale.getDefault(),
+                StandardCharsets.UTF_8);
+
+        this.fileManager = new InMemoryJavaFileManager(standardFileManager);
+    }
+
+    @Test
+    public void testIncrementalRemoveTransferable() throws Exception {
+        String testMessageGroup = "MsgGroup";
+        String testMessageGroupName = "GroupName";
+        String testMessageClass = "TestMessage";
+        String testMessageClass2 = "SomeMessage";
+
+        var compilationObjects1 = new ArrayList<JavaFileObject>();
+        JavaFileObject messageGroupObject = createMessageGroup(testMessageGroup, testMessageGroupName);
+        compilationObjects1.add(messageGroupObject);
+        compilationObjects1.add(createTransferable(testMessageClass));
+
+        Map<URI, JavaFileObject> compilation1 = compile(compilationObjects1);
+
+        JavaFileObject messageRegistry1 = compilation1.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry1.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        URI factoryUri = uriForJavaClass(
+                StandardLocation.SOURCE_OUTPUT,
+                RESOURCE_PACKAGE_NAME + "." + testMessageGroupName + "Factory",
+                Kind.SOURCE
+        );
+
+        String messageFactory1 = readJavaFileObject(compilation1.get(factoryUri));
+
+        assertTrue(messageFactory1.contains("TestMessageImpl.builder();"));
+
+        List<JavaFileObject> compilationObjects2 = new ArrayList<>();
+        compilationObjects2.add(createNonTransferable(testMessageClass));
+        compilationObjects2.add(createTransferable(testMessageClass2));
+
+        Map<URI, JavaFileObject> compilation2 = compile(compilationObjects2);
+
+        JavaFileObject messageRegistry2 = compilation2.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry2.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass2), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        String messageFactory2 = readJavaFileObject(compilation2.get(factoryUri));
+
+        assertFalse(messageFactory2.contains("TestMessageImpl.builder();"));
+        assertTrue(messageFactory2.contains("SomeMessageImpl.builder();"));
+    }
+
+    @Test
+    public void testIncrementalAddTransferable() throws Exception {
+        String testMessageGroup = "MsgGroup";
+        String testMessageGroupName = "GroupName";
+        String testMessageClass = "TestMessage";
+        String testMessageClass2 = "SomeMessage";
+
+        var compilationObjects1 = new ArrayList<JavaFileObject>();
+        JavaFileObject messageGroupObject = createMessageGroup(testMessageGroup, testMessageGroupName);
+        compilationObjects1.add(messageGroupObject);
+        compilationObjects1.add(createTransferable(testMessageClass));
+
+        Map<URI, JavaFileObject> compilation1 = compile(compilationObjects1);
+
+        JavaFileObject messageRegistry1 = compilation1.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry1.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        URI factoryUri = uriForJavaClass(
+                StandardLocation.SOURCE_OUTPUT,
+                RESOURCE_PACKAGE_NAME + "." + testMessageGroupName + "Factory",
+                Kind.SOURCE
+        );
+
+        String messageFactory1 = readJavaFileObject(compilation1.get(factoryUri));
+
+        assertTrue(messageFactory1.contains("TestMessageImpl.builder();"));
+
+        List<JavaFileObject> compilationObjects2 = new ArrayList<>();
+        compilationObjects2.add(createTransferable(testMessageClass2));
+
+        Map<URI, JavaFileObject> compilation2 = compile(compilationObjects2);
+
+        JavaFileObject messageRegistry2 = compilation2.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry2.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass2), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        String messageFactory2 = readJavaFileObject(compilation2.get(factoryUri));
+
+        assertTrue(messageFactory2.contains("TestMessageImpl.builder();"));
+        assertTrue(messageFactory2.contains("SomeMessageImpl.builder();"));
+    }
+
+    @Test
+    public void testChangeMessageGroup() throws Exception {
+        String testMessageGroup = "MsgGroup";
+        String testMessageGroupName = "GroupName";
+        String testMessageGroup2 = "MyNewGroup";
+        String testMessageGroupName2 = "NewGroupName";
+        String testMessageClass = "TestMessage";
+
+        var compilationObjects1 = new ArrayList<JavaFileObject>();
+        compilationObjects1.add(createMessageGroup(testMessageGroup, testMessageGroupName));
+        compilationObjects1.add(createTransferable(testMessageClass));
+
+        Map<URI, JavaFileObject> compilation1 = compile(compilationObjects1);
+
+        JavaFileObject messageRegistry1 = compilation1.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry1.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        URI factory1Uri = uriForJavaClass(
+                StandardLocation.SOURCE_OUTPUT,
+                RESOURCE_PACKAGE_NAME + "." + testMessageGroupName + "Factory",
+                Kind.SOURCE
+        );
+
+        String messageFactory1 = readJavaFileObject(compilation1.get(factory1Uri));
+
+        assertTrue(messageFactory1.contains("TestMessageImpl.builder();"));
+
+        List<JavaFileObject> compilationObjects2 = new ArrayList<>();
+        compilationObjects2.add(createMessageGroup(testMessageGroup2, testMessageGroupName2));
+        compilationObjects2.add(createTransferable(testMessageClass));
+
+        Map<URI, JavaFileObject> compilation2 = compile(compilationObjects2);
+
+        JavaFileObject messageRegistry2 = compilation2.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry2.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup2), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        URI factory2Uri = uriForJavaClass(
+                StandardLocation.SOURCE_OUTPUT,
+                RESOURCE_PACKAGE_NAME + "." + testMessageGroupName2 + "Factory",
+                Kind.SOURCE
+        );
+
+        String messageFactory2 = readJavaFileObject(compilation2.get(factory2Uri));
+
+        assertTrue(messageFactory2.contains("TestMessageImpl.builder();"));
+    }
+
+    private String readJavaFileObject(JavaFileObject object) throws Exception {
+        StringBuilder builder = new StringBuilder();
+        try (BufferedReader bufferedReader = new BufferedReader(object.openReader(true))) {
+            String line;
+
+            while ((line = bufferedReader.readLine()) != null) {
+                builder.append(line).append("\n");
+            }
+
+            return builder.toString();
+        }
+    }
+
+    private JavaFileObject createTransferable(String className) {
+        @Language("JAVA") String code =
+                "package " + RESOURCE_PACKAGE_NAME + ";\n"
+                  + "import org.apache.ignite.network.NetworkMessage;\n"
+                  + "import org.apache.ignite.network.annotations.Transferable;\n"
+                    + "\n"
+                + "\n"
+                + "@Transferable(value = 0)\n"
+                + "public interface " + className + " extends NetworkMessage {\n"
+                    + "    String foo();\n"
+                    + "}\n";
+        return JavaFileObjects.forSourceString(className, code);
+    }
+
+    private JavaFileObject createMessageGroup(String className, String groupName) {
+        @Language("JAVA") String code =
+                "package " + RESOURCE_PACKAGE_NAME + ";\n"
+                    + "\n"
+                    + "    import org.apache.ignite.network.annotations.MessageGroup;\n"
+                    + "\n"
+                    + "@MessageGroup(groupType = 1, groupName = \"" + groupName + "\")\n"
+                    + "public class " + className + " {\n"
+                    + "}\n";
+        return JavaFileObjects.forSourceString(className, code);
+    }
+
+    private JavaFileObject createNonTransferable(String className) {
+        @Language("JAVA") String code =
+            "package " + RESOURCE_PACKAGE_NAME + ";\n"
+                + "import org.apache.ignite.network.NetworkMessage;\n"
+                + "\n"
+                + "\n"
+                + "public interface " + className + " extends NetworkMessage {\n"
+                + "    String foo();\n"
+                + "}\n";
+        return JavaFileObjects.forSourceString(className, code);
+    }
+
+    private Map<URI, JavaFileObject> compile(Iterable<? extends JavaFileObject> files) {
+        JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler();
+        DiagnosticCollector<JavaFileObject> diagnosticListener = new DiagnosticCollector<>();
+        CompilationTask task = systemJavaCompiler
+                .getTask(null, fileManager, diagnosticListener, Collections.emptyList(), Set.of(), files);
+
+        task.setProcessors(Collections.singleton(new TransferableObjectProcessor()));
+
+        Boolean result = task.call();
+
+        assertTrue(result);
+
+        Iterable<JavaFileObject> generatedFiles = fileManager.getOutputFiles();
+
+        return StreamSupport.stream(generatedFiles.spliterator(), false)

Review comment:
       `generatedFiles` is already a `List`, there's no need for this code

##########
File path: modules/network/src/integrationTest/java/org/apache/ignite/internal/network/processor/ItTransferableObjectProcessorIncrementalTest.java
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static org.apache.ignite.internal.network.processor.IncrementalCompilationConfig.CONFIG_FILE_NAME;
+import static org.apache.ignite.internal.network.processor.IncrementalCompilationConfig.readClassName;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.testing.compile.JavaFileObjects;
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+import javax.tools.DiagnosticCollector;
+import javax.tools.FileObject;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaCompiler.CompilationTask;
+import javax.tools.JavaFileManager.Location;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+import org.intellij.lang.annotations.Language;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Integration tests for the {@link TransferableObjectProcessor} incremental compilation.
+ */
+public class ItTransferableObjectProcessorIncrementalTest {
+    /**
+     * Package name of the test sources.
+     */
+    private static final String RESOURCE_PACKAGE_NAME = "org.apache.ignite.internal.network.processor";
+
+    /** File manager for incremental compilation. */
+    private InMemoryJavaFileManager fileManager;
+
+    @BeforeEach
+    void setUp() {
+        DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
+        JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler();
+        StandardJavaFileManager standardFileManager = systemJavaCompiler.getStandardFileManager(diagnosticCollector, Locale.getDefault(),
+                StandardCharsets.UTF_8);
+
+        this.fileManager = new InMemoryJavaFileManager(standardFileManager);
+    }
+
+    @Test
+    public void testIncrementalRemoveTransferable() throws Exception {
+        String testMessageGroup = "MsgGroup";
+        String testMessageGroupName = "GroupName";
+        String testMessageClass = "TestMessage";
+        String testMessageClass2 = "SomeMessage";
+
+        var compilationObjects1 = new ArrayList<JavaFileObject>();
+        JavaFileObject messageGroupObject = createMessageGroup(testMessageGroup, testMessageGroupName);
+        compilationObjects1.add(messageGroupObject);
+        compilationObjects1.add(createTransferable(testMessageClass));
+
+        Map<URI, JavaFileObject> compilation1 = compile(compilationObjects1);
+
+        JavaFileObject messageRegistry1 = compilation1.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry1.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        URI factoryUri = uriForJavaClass(
+                StandardLocation.SOURCE_OUTPUT,
+                RESOURCE_PACKAGE_NAME + "." + testMessageGroupName + "Factory",
+                Kind.SOURCE
+        );
+
+        String messageFactory1 = readJavaFileObject(compilation1.get(factoryUri));
+
+        assertTrue(messageFactory1.contains("TestMessageImpl.builder();"));
+
+        List<JavaFileObject> compilationObjects2 = new ArrayList<>();
+        compilationObjects2.add(createNonTransferable(testMessageClass));
+        compilationObjects2.add(createTransferable(testMessageClass2));
+
+        Map<URI, JavaFileObject> compilation2 = compile(compilationObjects2);
+
+        JavaFileObject messageRegistry2 = compilation2.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry2.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass2), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        String messageFactory2 = readJavaFileObject(compilation2.get(factoryUri));
+
+        assertFalse(messageFactory2.contains("TestMessageImpl.builder();"));
+        assertTrue(messageFactory2.contains("SomeMessageImpl.builder();"));
+    }
+
+    @Test
+    public void testIncrementalAddTransferable() throws Exception {
+        String testMessageGroup = "MsgGroup";
+        String testMessageGroupName = "GroupName";
+        String testMessageClass = "TestMessage";
+        String testMessageClass2 = "SomeMessage";
+
+        var compilationObjects1 = new ArrayList<JavaFileObject>();
+        JavaFileObject messageGroupObject = createMessageGroup(testMessageGroup, testMessageGroupName);
+        compilationObjects1.add(messageGroupObject);
+        compilationObjects1.add(createTransferable(testMessageClass));
+
+        Map<URI, JavaFileObject> compilation1 = compile(compilationObjects1);
+
+        JavaFileObject messageRegistry1 = compilation1.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry1.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        URI factoryUri = uriForJavaClass(
+                StandardLocation.SOURCE_OUTPUT,
+                RESOURCE_PACKAGE_NAME + "." + testMessageGroupName + "Factory",
+                Kind.SOURCE
+        );
+
+        String messageFactory1 = readJavaFileObject(compilation1.get(factoryUri));
+
+        assertTrue(messageFactory1.contains("TestMessageImpl.builder();"));
+
+        List<JavaFileObject> compilationObjects2 = new ArrayList<>();
+        compilationObjects2.add(createTransferable(testMessageClass2));
+
+        Map<URI, JavaFileObject> compilation2 = compile(compilationObjects2);
+
+        JavaFileObject messageRegistry2 = compilation2.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry2.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass2), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        String messageFactory2 = readJavaFileObject(compilation2.get(factoryUri));
+
+        assertTrue(messageFactory2.contains("TestMessageImpl.builder();"));
+        assertTrue(messageFactory2.contains("SomeMessageImpl.builder();"));
+    }
+
+    @Test
+    public void testChangeMessageGroup() throws Exception {
+        String testMessageGroup = "MsgGroup";
+        String testMessageGroupName = "GroupName";
+        String testMessageGroup2 = "MyNewGroup";
+        String testMessageGroupName2 = "NewGroupName";
+        String testMessageClass = "TestMessage";
+
+        var compilationObjects1 = new ArrayList<JavaFileObject>();
+        compilationObjects1.add(createMessageGroup(testMessageGroup, testMessageGroupName));
+        compilationObjects1.add(createTransferable(testMessageClass));
+
+        Map<URI, JavaFileObject> compilation1 = compile(compilationObjects1);
+
+        JavaFileObject messageRegistry1 = compilation1.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry1.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        URI factory1Uri = uriForJavaClass(
+                StandardLocation.SOURCE_OUTPUT,
+                RESOURCE_PACKAGE_NAME + "." + testMessageGroupName + "Factory",
+                Kind.SOURCE
+        );
+
+        String messageFactory1 = readJavaFileObject(compilation1.get(factory1Uri));
+
+        assertTrue(messageFactory1.contains("TestMessageImpl.builder();"));
+
+        List<JavaFileObject> compilationObjects2 = new ArrayList<>();
+        compilationObjects2.add(createMessageGroup(testMessageGroup2, testMessageGroupName2));
+        compilationObjects2.add(createTransferable(testMessageClass));
+
+        Map<URI, JavaFileObject> compilation2 = compile(compilationObjects2);
+
+        JavaFileObject messageRegistry2 = compilation2.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry2.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup2), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        URI factory2Uri = uriForJavaClass(
+                StandardLocation.SOURCE_OUTPUT,
+                RESOURCE_PACKAGE_NAME + "." + testMessageGroupName2 + "Factory",
+                Kind.SOURCE
+        );
+
+        String messageFactory2 = readJavaFileObject(compilation2.get(factory2Uri));
+
+        assertTrue(messageFactory2.contains("TestMessageImpl.builder();"));
+    }
+
+    private String readJavaFileObject(JavaFileObject object) throws Exception {
+        StringBuilder builder = new StringBuilder();
+        try (BufferedReader bufferedReader = new BufferedReader(object.openReader(true))) {
+            String line;
+
+            while ((line = bufferedReader.readLine()) != null) {
+                builder.append(line).append("\n");
+            }
+
+            return builder.toString();
+        }
+    }
+
+    private JavaFileObject createTransferable(String className) {
+        @Language("JAVA") String code =
+                "package " + RESOURCE_PACKAGE_NAME + ";\n"
+                  + "import org.apache.ignite.network.NetworkMessage;\n"
+                  + "import org.apache.ignite.network.annotations.Transferable;\n"
+                    + "\n"
+                + "\n"
+                + "@Transferable(value = 0)\n"
+                + "public interface " + className + " extends NetworkMessage {\n"
+                    + "    String foo();\n"
+                    + "}\n";
+        return JavaFileObjects.forSourceString(className, code);
+    }
+
+    private JavaFileObject createMessageGroup(String className, String groupName) {
+        @Language("JAVA") String code =
+                "package " + RESOURCE_PACKAGE_NAME + ";\n"
+                    + "\n"
+                    + "    import org.apache.ignite.network.annotations.MessageGroup;\n"
+                    + "\n"
+                    + "@MessageGroup(groupType = 1, groupName = \"" + groupName + "\")\n"
+                    + "public class " + className + " {\n"
+                    + "}\n";
+        return JavaFileObjects.forSourceString(className, code);
+    }
+
+    private JavaFileObject createNonTransferable(String className) {
+        @Language("JAVA") String code =
+            "package " + RESOURCE_PACKAGE_NAME + ";\n"
+                + "import org.apache.ignite.network.NetworkMessage;\n"
+                + "\n"
+                + "\n"
+                + "public interface " + className + " extends NetworkMessage {\n"
+                + "    String foo();\n"
+                + "}\n";
+        return JavaFileObjects.forSourceString(className, code);
+    }
+
+    private Map<URI, JavaFileObject> compile(Iterable<? extends JavaFileObject> files) {
+        JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler();
+        DiagnosticCollector<JavaFileObject> diagnosticListener = new DiagnosticCollector<>();
+        CompilationTask task = systemJavaCompiler
+                .getTask(null, fileManager, diagnosticListener, Collections.emptyList(), Set.of(), files);
+
+        task.setProcessors(Collections.singleton(new TransferableObjectProcessor()));
+
+        Boolean result = task.call();
+
+        assertTrue(result);
+
+        Iterable<JavaFileObject> generatedFiles = fileManager.getOutputFiles();
+
+        return StreamSupport.stream(generatedFiles.spliterator(), false)
+                .collect(Collectors.toMap(FileObject::toUri, Function.identity()));
+    }
+
+
+    private static URI uriForMessagesFile() {
+        return uriForObject(StandardLocation.CLASS_OUTPUT, CONFIG_FILE_NAME, Kind.OTHER);
+    }
+
+    private static URI uriForJavaClass(Location location, String className, Kind kind) {

Review comment:
       These methods are copy-pasted from the file manager, let's reuse them

##########
File path: modules/network/src/integrationTest/java/org/apache/ignite/internal/network/processor/InMemoryJavaFileManager.java
##########
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.io.ByteSource;
+import com.google.testing.compile.ForwardingStandardJavaFileManager;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import javax.tools.FileObject;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+
+/**
+ * A file manager implementation that stores all output in memory.
+ */
+final class InMemoryJavaFileManager extends ForwardingStandardJavaFileManager {
+
+    private final LoadingCache<URI, JavaFileObject> inMemoryFileObjects = CacheBuilder.newBuilder()
+            .build(new CacheLoader<>() {
+                @Override
+                public JavaFileObject load(URI key) {
+                    return new InMemoryJavaFileObject(key);
+                }
+            });
+
+    InMemoryJavaFileManager(StandardJavaFileManager fileManager) {
+        super(fileManager);
+    }
+
+    private static URI uriForFileObject(Location location, String packageName, String relativeName) {
+        StringBuilder uri = new StringBuilder("mem:///").append(location.getName()).append('/');
+        if (!packageName.isEmpty()) {
+            uri.append(packageName.replace('.', '/')).append('/');
+        }
+        uri.append(relativeName);
+        return URI.create(uri.toString());
+    }
+
+    private static URI uriForJavaFileObject(Location location, String className, Kind kind) {
+        return URI.create("mem:///" + location.getName() + '/' + className.replace('.', '/') + kind.extension);
+    }
+
+    @Override
+    public boolean isSameFile(FileObject a, FileObject b) {
+        /* This check is less strict than what is typically done by the normal compiler file managers
+         * (e.g. JavacFileManager), but is actually the moral equivalent of what most of the
+         * implementations do anyway. We use this check rather than just delegating to the compiler's
+         * file manager because file objects for tests generally cause IllegalArgumentExceptions. */
+        return a.toUri().equals(b.toUri());
+    }
+
+    @Override
+    public String inferBinaryName(Location location, JavaFileObject file) {
+        if (file instanceof InMemoryJavaFileObject) {
+            if (location == StandardLocation.CLASS_OUTPUT || location == StandardLocation.CLASS_PATH
+                    || location == StandardLocation.SOURCE_OUTPUT) {
+                return toBinaryName(file);
+            } else {
+                return null;
+            }
+        }
+        return super.inferBinaryName(location, file);
+    }
+
+    private static String toBinaryName(JavaFileObject file) {
+        String fileName = file.getName();
+        // We are only interested in "org.apache..."
+        return fileName.substring(fileName.indexOf("org"), fileName.lastIndexOf('.')).replace('/', '.');
+    }
+
+    @Override
+    public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
+        if (location.isOutputLocation()) {
+            return inMemoryFileObjects.getIfPresent(uriForFileObject(location, packageName, relativeName));
+        } else {
+            return super.getFileForInput(location, packageName, relativeName);
+        }
+    }
+
+    @Override
+    public JavaFileObject getJavaFileForInput(Location location, String className, Kind kind) throws IOException {
+        if (location.isOutputLocation()) {
+            return inMemoryFileObjects.getIfPresent(uriForJavaFileObject(location, className, kind));
+        } else {
+            return super.getJavaFileForInput(location, className, kind);
+        }
+    }
+
+    @Override
+    public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) {
+        URI uri = uriForFileObject(location, packageName, relativeName);
+        return inMemoryFileObjects.getUnchecked(uri);
+    }
+
+    @Override
+    public JavaFileObject getJavaFileForOutput(Location location, String className, final Kind kind, FileObject sibling) {
+        URI uri = uriForJavaFileObject(location, className, kind);
+        return inMemoryFileObjects.getUnchecked(uri);
+    }
+
+    @Override
+    public Iterable<JavaFileObject> list(Location location, String packageName, Set<Kind> kinds, boolean recurse) throws IOException {
+        List<JavaFileObject> list = new ArrayList<>();
+
+        super.list(location, packageName, kinds, recurse).forEach(list::add);

Review comment:
       `list` returns an `Iterable`, this code will not compile

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.
+ */
+class IncrementalCompilationConfig {
+    /** Incremental compilation configuration file name. */
+    static final String CONFIG_FILE_NAME = "META-INF/transferable.messages";
+
+    /** Message group class name. */
+    private ClassName messageGroupClassName;
+
+    /** Messages. */
+    private final List<ClassName> messageClasses;
+
+    IncrementalCompilationConfig(ClassName messageGroupClassName, List<ClassName> messageClasses) {
+        this.messageGroupClassName = messageGroupClassName;
+        this.messageClasses = messageClasses;
+    }
+
+    /**
+     * Saves configuration on disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    void writeConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject fileObject;
+        try {
+            fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+
+        try (OutputStream out = fileObject.openOutputStream()) {
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+            writeClassName(writer, messageGroupClassName);
+            writer.newLine();
+
+            for (ClassName messageClassName : messageClasses) {
+                writeClassName(writer, messageClassName);
+                writer.newLine();
+            }
+
+            writer.flush();
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+    }
+
+    /**
+     * Reads configuration from disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    static IncrementalCompilationConfig readConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject resource;
+
+        try {
+            resource = filer.getResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            return null;
+        }
+
+        try (Reader reader = resource.openReader(true)) {
+            BufferedReader bufferedReader = new BufferedReader(reader);
+            String messageClassNameString = bufferedReader.readLine();
+
+            if (messageClassNameString == null) {
+                return null;
+            }
+
+            ClassName messageClassName = readClassName(messageClassNameString);
+
+            List<ClassName> message = new ArrayList<>();
+
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                ClassName className = readClassName(line);
+                message.add(className);
+            }
+
+            return new IncrementalCompilationConfig(messageClassName, message);
+        } catch (FileNotFoundException | NoSuchFileException e) {
+            return null;
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+    }
+
+    /**
+     * Writes class name with all the enclosing classes.
+     *
+     * @param writer Writer.
+     * @param className Class name.
+     * @throws IOException If failed.
+     */
+    private void writeClassName(BufferedWriter writer, ClassName className) throws IOException {
+        writer.write(className.packageName());
+        writer.write(' ');
+
+        List<String> enclosingSimpleNames = new ArrayList<>();
+        ClassName enclosing = className;
+        while ((enclosing = enclosing.enclosingClassName()) != null) {

Review comment:
       There's a `ClassName#simpleNames()` method which does the exact same thing

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.
+ */
+class IncrementalCompilationConfig {
+    /** Incremental compilation configuration file name. */
+    static final String CONFIG_FILE_NAME = "META-INF/transferable.messages";
+
+    /** Message group class name. */
+    private ClassName messageGroupClassName;
+
+    /** Messages. */
+    private final List<ClassName> messageClasses;
+
+    IncrementalCompilationConfig(ClassName messageGroupClassName, List<ClassName> messageClasses) {
+        this.messageGroupClassName = messageGroupClassName;
+        this.messageClasses = messageClasses;
+    }
+
+    /**
+     * Saves configuration on disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    void writeConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject fileObject;
+        try {
+            fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+
+        try (OutputStream out = fileObject.openOutputStream()) {
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+            writeClassName(writer, messageGroupClassName);
+            writer.newLine();
+
+            for (ClassName messageClassName : messageClasses) {
+                writeClassName(writer, messageClassName);
+                writer.newLine();
+            }
+
+            writer.flush();
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+    }
+
+    /**
+     * Reads configuration from disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    static IncrementalCompilationConfig readConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject resource;
+
+        try {
+            resource = filer.getResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            return null;
+        }
+
+        try (Reader reader = resource.openReader(true)) {
+            BufferedReader bufferedReader = new BufferedReader(reader);
+            String messageClassNameString = bufferedReader.readLine();
+
+            if (messageClassNameString == null) {
+                return null;
+            }
+
+            ClassName messageClassName = readClassName(messageClassNameString);
+
+            List<ClassName> message = new ArrayList<>();
+
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                ClassName className = readClassName(line);
+                message.add(className);
+            }
+
+            return new IncrementalCompilationConfig(messageClassName, message);
+        } catch (FileNotFoundException | NoSuchFileException e) {
+            return null;
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+    }
+
+    /**
+     * Writes class name with all the enclosing classes.
+     *
+     * @param writer Writer.
+     * @param className Class name.
+     * @throws IOException If failed.
+     */
+    private void writeClassName(BufferedWriter writer, ClassName className) throws IOException {

Review comment:
       can be `static`

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.
+ */
+class IncrementalCompilationConfig {
+    /** Incremental compilation configuration file name. */
+    static final String CONFIG_FILE_NAME = "META-INF/transferable.messages";
+
+    /** Message group class name. */
+    private ClassName messageGroupClassName;
+
+    /** Messages. */
+    private final List<ClassName> messageClasses;
+
+    IncrementalCompilationConfig(ClassName messageGroupClassName, List<ClassName> messageClasses) {
+        this.messageGroupClassName = messageGroupClassName;
+        this.messageClasses = messageClasses;
+    }
+
+    /**
+     * Saves configuration on disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    void writeConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject fileObject;
+        try {
+            fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+
+        try (OutputStream out = fileObject.openOutputStream()) {
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+            writeClassName(writer, messageGroupClassName);
+            writer.newLine();
+
+            for (ClassName messageClassName : messageClasses) {
+                writeClassName(writer, messageClassName);
+                writer.newLine();
+            }
+
+            writer.flush();
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+    }
+
+    /**
+     * Reads configuration from disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    static IncrementalCompilationConfig readConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject resource;
+
+        try {
+            resource = filer.getResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            return null;
+        }
+
+        try (Reader reader = resource.openReader(true)) {
+            BufferedReader bufferedReader = new BufferedReader(reader);
+            String messageClassNameString = bufferedReader.readLine();
+
+            if (messageClassNameString == null) {
+                return null;
+            }
+
+            ClassName messageClassName = readClassName(messageClassNameString);
+
+            List<ClassName> message = new ArrayList<>();
+
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                ClassName className = readClassName(line);
+                message.add(className);
+            }
+
+            return new IncrementalCompilationConfig(messageClassName, message);
+        } catch (FileNotFoundException | NoSuchFileException e) {
+            return null;
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+    }
+
+    /**
+     * Writes class name with all the enclosing classes.
+     *
+     * @param writer Writer.
+     * @param className Class name.
+     * @throws IOException If failed.
+     */
+    private void writeClassName(BufferedWriter writer, ClassName className) throws IOException {
+        writer.write(className.packageName());
+        writer.write(' ');
+
+        List<String> enclosingSimpleNames = new ArrayList<>();

Review comment:
       Did you copy this code from somewhere? It's missing the empty lines around statements that we have in Ignite code

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/MessageClass.java
##########
@@ -191,7 +228,7 @@ public boolean isAutoSerializable() {
      *
      * @return A copy of the given string with the first character converted to lower case.
      */
-    private static String decapitalize(String str) {
+    public static String decapitalize(String str) {

Review comment:
       this method can remain `private`

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/TransferableObjectProcessor.java
##########
@@ -263,9 +302,18 @@ private MessageGroupWrapper getMessageGroup(RoundEnvironment roundEnv) {
             ));
         }
 
-        Element singleElement = messageGroupSet.iterator().next();
+        TypeElement groupElement = (TypeElement) messageGroupSet.iterator().next();
+
+        if (config != null) {
+            TypeElement groupFromConfig = elementUtils.getTypeElement(config.messageGroupClassName().canonicalName());
+
+            // TypeElement is not overriding equals, but same type elements should be equal by pointer
+            if (groupFromConfig != null && !Objects.equals(groupFromConfig, groupElement)) {

Review comment:
       should we use `==` then, instead of `equals`?

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/messages/MessageFactoryGenerator.java
##########
@@ -57,7 +60,7 @@ public MessageFactoryGenerator(
      * @param messages Network Messages from a module
      * @return {@code TypeSpec} of the generated message factory
      */
-    public TypeSpec generateMessageFactory(List<MessageClass> messages) {
+    public TypeSpec generateMessageFactory(List<ClassName> messages) {

Review comment:
       I think this method should still accept `List<MessageClass>`

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.
+ */
+class IncrementalCompilationConfig {
+    /** Incremental compilation configuration file name. */
+    static final String CONFIG_FILE_NAME = "META-INF/transferable.messages";
+
+    /** Message group class name. */
+    private ClassName messageGroupClassName;
+
+    /** Messages. */
+    private final List<ClassName> messageClasses;
+
+    IncrementalCompilationConfig(ClassName messageGroupClassName, List<ClassName> messageClasses) {
+        this.messageGroupClassName = messageGroupClassName;
+        this.messageClasses = messageClasses;
+    }
+
+    /**
+     * Saves configuration on disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    void writeConfig(ProcessingEnvironment processingEnv) {
+        Filer filer = processingEnv.getFiler();
+
+        FileObject fileObject;
+        try {
+            fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", CONFIG_FILE_NAME);
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+
+        try (OutputStream out = fileObject.openOutputStream()) {
+            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+            writeClassName(writer, messageGroupClassName);
+            writer.newLine();
+
+            for (ClassName messageClassName : messageClasses) {
+                writeClassName(writer, messageClassName);
+                writer.newLine();
+            }
+
+            writer.flush();
+        } catch (IOException e) {
+            throw new ProcessingException(e.getMessage());
+        }
+    }
+
+    /**
+     * Reads configuration from disk.
+     *
+     * @param processingEnv Processing environment.
+     */
+    static IncrementalCompilationConfig readConfig(ProcessingEnvironment processingEnv) {

Review comment:
       should be marked with `@Nullable`

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.
+ */
+class IncrementalCompilationConfig {
+    /** Incremental compilation configuration file name. */
+    static final String CONFIG_FILE_NAME = "META-INF/transferable.messages";
+
+    /** Message group class name. */
+    private ClassName messageGroupClassName;
+
+    /** Messages. */
+    private final List<ClassName> messageClasses;

Review comment:
       Or maybe it's even better to make this class immutable and to construct a new config explicitly when needed, because currently this class represents a wrapper around a file

##########
File path: modules/network/src/integrationTest/java/org/apache/ignite/internal/network/processor/ItTransferableObjectProcessorIncrementalTest.java
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static org.apache.ignite.internal.network.processor.IncrementalCompilationConfig.CONFIG_FILE_NAME;
+import static org.apache.ignite.internal.network.processor.IncrementalCompilationConfig.readClassName;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.testing.compile.JavaFileObjects;
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+import javax.tools.DiagnosticCollector;
+import javax.tools.FileObject;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaCompiler.CompilationTask;
+import javax.tools.JavaFileManager.Location;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+import org.intellij.lang.annotations.Language;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Integration tests for the {@link TransferableObjectProcessor} incremental compilation.
+ */
+public class ItTransferableObjectProcessorIncrementalTest {
+    /**
+     * Package name of the test sources.
+     */
+    private static final String RESOURCE_PACKAGE_NAME = "org.apache.ignite.internal.network.processor";
+
+    /** File manager for incremental compilation. */
+    private InMemoryJavaFileManager fileManager;
+
+    @BeforeEach
+    void setUp() {
+        DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
+        JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler();
+        StandardJavaFileManager standardFileManager = systemJavaCompiler.getStandardFileManager(diagnosticCollector, Locale.getDefault(),
+                StandardCharsets.UTF_8);
+
+        this.fileManager = new InMemoryJavaFileManager(standardFileManager);
+    }
+
+    @Test
+    public void testIncrementalRemoveTransferable() throws Exception {
+        String testMessageGroup = "MsgGroup";
+        String testMessageGroupName = "GroupName";
+        String testMessageClass = "TestMessage";
+        String testMessageClass2 = "SomeMessage";
+
+        var compilationObjects1 = new ArrayList<JavaFileObject>();
+        JavaFileObject messageGroupObject = createMessageGroup(testMessageGroup, testMessageGroupName);
+        compilationObjects1.add(messageGroupObject);
+        compilationObjects1.add(createTransferable(testMessageClass));
+
+        Map<URI, JavaFileObject> compilation1 = compile(compilationObjects1);
+
+        JavaFileObject messageRegistry1 = compilation1.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry1.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        URI factoryUri = uriForJavaClass(
+                StandardLocation.SOURCE_OUTPUT,
+                RESOURCE_PACKAGE_NAME + "." + testMessageGroupName + "Factory",
+                Kind.SOURCE
+        );
+
+        String messageFactory1 = readJavaFileObject(compilation1.get(factoryUri));
+
+        assertTrue(messageFactory1.contains("TestMessageImpl.builder();"));
+
+        List<JavaFileObject> compilationObjects2 = new ArrayList<>();
+        compilationObjects2.add(createNonTransferable(testMessageClass));
+        compilationObjects2.add(createTransferable(testMessageClass2));
+
+        Map<URI, JavaFileObject> compilation2 = compile(compilationObjects2);
+
+        JavaFileObject messageRegistry2 = compilation2.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry2.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass2), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        String messageFactory2 = readJavaFileObject(compilation2.get(factoryUri));
+
+        assertFalse(messageFactory2.contains("TestMessageImpl.builder();"));
+        assertTrue(messageFactory2.contains("SomeMessageImpl.builder();"));
+    }
+
+    @Test
+    public void testIncrementalAddTransferable() throws Exception {
+        String testMessageGroup = "MsgGroup";
+        String testMessageGroupName = "GroupName";
+        String testMessageClass = "TestMessage";
+        String testMessageClass2 = "SomeMessage";
+
+        var compilationObjects1 = new ArrayList<JavaFileObject>();
+        JavaFileObject messageGroupObject = createMessageGroup(testMessageGroup, testMessageGroupName);
+        compilationObjects1.add(messageGroupObject);
+        compilationObjects1.add(createTransferable(testMessageClass));
+
+        Map<URI, JavaFileObject> compilation1 = compile(compilationObjects1);
+
+        JavaFileObject messageRegistry1 = compilation1.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry1.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        URI factoryUri = uriForJavaClass(
+                StandardLocation.SOURCE_OUTPUT,
+                RESOURCE_PACKAGE_NAME + "." + testMessageGroupName + "Factory",
+                Kind.SOURCE
+        );
+
+        String messageFactory1 = readJavaFileObject(compilation1.get(factoryUri));
+
+        assertTrue(messageFactory1.contains("TestMessageImpl.builder();"));
+
+        List<JavaFileObject> compilationObjects2 = new ArrayList<>();
+        compilationObjects2.add(createTransferable(testMessageClass2));
+
+        Map<URI, JavaFileObject> compilation2 = compile(compilationObjects2);
+
+        JavaFileObject messageRegistry2 = compilation2.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry2.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass2), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        String messageFactory2 = readJavaFileObject(compilation2.get(factoryUri));
+
+        assertTrue(messageFactory2.contains("TestMessageImpl.builder();"));
+        assertTrue(messageFactory2.contains("SomeMessageImpl.builder();"));
+    }
+
+    @Test
+    public void testChangeMessageGroup() throws Exception {
+        String testMessageGroup = "MsgGroup";
+        String testMessageGroupName = "GroupName";
+        String testMessageGroup2 = "MyNewGroup";
+        String testMessageGroupName2 = "NewGroupName";
+        String testMessageClass = "TestMessage";
+
+        var compilationObjects1 = new ArrayList<JavaFileObject>();
+        compilationObjects1.add(createMessageGroup(testMessageGroup, testMessageGroupName));
+        compilationObjects1.add(createTransferable(testMessageClass));
+
+        Map<URI, JavaFileObject> compilation1 = compile(compilationObjects1);
+
+        JavaFileObject messageRegistry1 = compilation1.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry1.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        URI factory1Uri = uriForJavaClass(
+                StandardLocation.SOURCE_OUTPUT,
+                RESOURCE_PACKAGE_NAME + "." + testMessageGroupName + "Factory",
+                Kind.SOURCE
+        );
+
+        String messageFactory1 = readJavaFileObject(compilation1.get(factory1Uri));
+
+        assertTrue(messageFactory1.contains("TestMessageImpl.builder();"));
+
+        List<JavaFileObject> compilationObjects2 = new ArrayList<>();
+        compilationObjects2.add(createMessageGroup(testMessageGroup2, testMessageGroupName2));
+        compilationObjects2.add(createTransferable(testMessageClass));
+
+        Map<URI, JavaFileObject> compilation2 = compile(compilationObjects2);
+
+        JavaFileObject messageRegistry2 = compilation2.get(uriForMessagesFile());
+        try (BufferedReader bufferedReader = new BufferedReader(messageRegistry2.openReader(true))) {
+            ClassName messageGroupClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageGroup2), messageGroupClass);
+
+            ClassName messageClass = readClassName(bufferedReader.readLine());
+
+            assertEquals(ClassName.get(RESOURCE_PACKAGE_NAME, testMessageClass), messageClass);
+
+            assertNull(bufferedReader.readLine());
+        }
+
+        URI factory2Uri = uriForJavaClass(
+                StandardLocation.SOURCE_OUTPUT,
+                RESOURCE_PACKAGE_NAME + "." + testMessageGroupName2 + "Factory",
+                Kind.SOURCE
+        );
+
+        String messageFactory2 = readJavaFileObject(compilation2.get(factory2Uri));
+
+        assertTrue(messageFactory2.contains("TestMessageImpl.builder();"));
+    }
+
+    private String readJavaFileObject(JavaFileObject object) throws Exception {
+        StringBuilder builder = new StringBuilder();
+        try (BufferedReader bufferedReader = new BufferedReader(object.openReader(true))) {
+            String line;
+
+            while ((line = bufferedReader.readLine()) != null) {
+                builder.append(line).append("\n");
+            }
+
+            return builder.toString();
+        }
+    }
+
+    private JavaFileObject createTransferable(String className) {
+        @Language("JAVA") String code =
+                "package " + RESOURCE_PACKAGE_NAME + ";\n"
+                  + "import org.apache.ignite.network.NetworkMessage;\n"
+                  + "import org.apache.ignite.network.annotations.Transferable;\n"
+                    + "\n"
+                + "\n"
+                + "@Transferable(value = 0)\n"
+                + "public interface " + className + " extends NetworkMessage {\n"
+                    + "    String foo();\n"
+                    + "}\n";
+        return JavaFileObjects.forSourceString(className, code);
+    }
+
+    private JavaFileObject createMessageGroup(String className, String groupName) {
+        @Language("JAVA") String code =
+                "package " + RESOURCE_PACKAGE_NAME + ";\n"
+                    + "\n"
+                    + "    import org.apache.ignite.network.annotations.MessageGroup;\n"
+                    + "\n"
+                    + "@MessageGroup(groupType = 1, groupName = \"" + groupName + "\")\n"
+                    + "public class " + className + " {\n"
+                    + "}\n";
+        return JavaFileObjects.forSourceString(className, code);
+    }
+
+    private JavaFileObject createNonTransferable(String className) {
+        @Language("JAVA") String code =
+            "package " + RESOURCE_PACKAGE_NAME + ";\n"
+                + "import org.apache.ignite.network.NetworkMessage;\n"
+                + "\n"
+                + "\n"
+                + "public interface " + className + " extends NetworkMessage {\n"
+                + "    String foo();\n"
+                + "}\n";
+        return JavaFileObjects.forSourceString(className, code);
+    }
+
+    private Map<URI, JavaFileObject> compile(Iterable<? extends JavaFileObject> files) {
+        JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler();
+        DiagnosticCollector<JavaFileObject> diagnosticListener = new DiagnosticCollector<>();

Review comment:
       I think this object should be a field of this class

##########
File path: modules/network/src/integrationTest/java/org/apache/ignite/internal/network/processor/InMemoryJavaFileManager.java
##########
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.io.ByteSource;
+import com.google.testing.compile.ForwardingStandardJavaFileManager;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import javax.tools.FileObject;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+
+/**
+ * A file manager implementation that stores all output in memory.
+ */
+final class InMemoryJavaFileManager extends ForwardingStandardJavaFileManager {

Review comment:
       I've tried to code a simpler version without any Guava:
   ```
   final class InMemoryJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
       private final Map<URI, JavaFileObject> cache = new HashMap<>();
   
       InMemoryJavaFileManager(JavaFileManager fileManager) {
           super(fileManager);
       }
   
       private static URI uriForFileObject(Location location, String packageName, String relativeName) {
           StringBuilder uri = new StringBuilder("mem:///").append(location.getName()).append('/');
           if (!packageName.isEmpty()) {
               uri.append(packageName.replace('.', '/')).append('/');
           }
           uri.append(relativeName);
           return URI.create(uri.toString());
       }
   
       private static URI uriForJavaFileObject(Location location, String className, Kind kind) {
           return URI.create("mem:///" + location.getName() + '/' + className.replace('.', '/') + kind.extension);
       }
   
       @Override
       public String inferBinaryName(Location location, JavaFileObject file) {
           if (file instanceof InMemoryJavaFileObject) {
               String fileName = file.getName();
               // We are only interested in "org.apache..."
               return fileName.substring(fileName.indexOf("org"), fileName.lastIndexOf('.')).replace('/', '.');
           }
   
           return super.inferBinaryName(location, file);
       }
   
       @Override
       public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) {
           return cache.computeIfAbsent(
                   uriForFileObject(location, packageName, relativeName),
                   uri -> new InMemoryJavaFileObject(uri, Kind.OTHER)
           );
       }
   
       @Override
       public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) {
           return cache.computeIfAbsent(
                   uriForJavaFileObject(location, className, kind),
                   uri -> new InMemoryJavaFileObject(uri, kind)
           );
       }
   
       @Override
       public Iterable<JavaFileObject> list(Location location, String packageName, Set<Kind> kinds, boolean recurse) throws IOException {
           List<JavaFileObject> list = new ArrayList<>();
   
           super.list(location, packageName, kinds, recurse).forEach(list::add);
   
           if (location == StandardLocation.CLASS_OUTPUT || location == StandardLocation.CLASS_PATH
                   || location == StandardLocation.SOURCE_OUTPUT) {
               list.addAll(cache.values());
           }
   
           return list;
       }
   
       Map<URI, JavaFileObject> getOutputFiles() {
           return cache;
       }
   
       private static final class InMemoryJavaFileObject extends SimpleJavaFileObject {
           private byte[] data;
   
           InMemoryJavaFileObject(URI uri, Kind kind) {
               super(uri, kind);
           }
   
           @Override
           public InputStream openInputStream() throws IOException {
               return new ByteArrayInputStream(data());
           }
   
           @Override
           public OutputStream openOutputStream() {
               return new ByteArrayOutputStream() {
                   @Override
                   public void close() throws IOException {
                       super.close();
   
                       data = toByteArray();
                   }
               };
           }
   
           @Override
           public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
               return new String(data(), StandardCharsets.UTF_8);
           }
   
           private byte[] data() throws IOException {
               if (data == null) {
                   throw new FileNotFoundException();
               }
   
               return data;
           }
       }
   }
   ```

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/IncrementalCompilationConfig.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.squareup.javapoet.ClassName;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.NoSuchFileException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Incremental configuration of the {@link TransferableObjectProcessor}.
+ * Holds data between (re-)compilations.
+ */
+class IncrementalCompilationConfig {
+    /** Incremental compilation configuration file name. */
+    static final String CONFIG_FILE_NAME = "META-INF/transferable.messages";
+
+    /** Message group class name. */
+    private ClassName messageGroupClassName;
+
+    /** Messages. */
+    private final List<ClassName> messageClasses;

Review comment:
       that's actually very confusing. I would prefer adding special modification methods to this class rather than having it modified from outside

##########
File path: modules/network-annotation-processor/src/main/java/org/apache/ignite/internal/network/processor/MessageClass.java
##########
@@ -141,13 +141,36 @@ public String simpleName() {
         return getters;
     }
 
+    /**
+     * Returns class name that the generated SerializationFactory should have.
+     *
+     * @return Class name that the generated SerializationFactory should have.
+     */
+    public ClassName serializationFactoryName() {
+        return serializationFactoryName(className);
+    }
+
+    /**
+     * Implementation of the {@link MessageClass#serializationFactoryName()}.
+     */
+    public static ClassName serializationFactoryName(ClassName className) {

Review comment:
       or maybe these methods are not needed at all and you can use the `MessageClass` instead




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org