Here is my current solution, that is imperfect but I stuck with it as I don't have better ideas yet.
I want to move memory allocation outside of the catch block, so the memory allocation does not happen in catch block.
Object[] array in each repositoryHere is the code:
@Repository
public class UserRepositoryImpl implements UserRepository {
    private static final Object[] USER_ENTITY_ARG = new Object[]{User.KIND};
    
    private final Datastore datastore;
    private final KeyFactory keyFactory;
    private final MessageSource messageSource;
    
    // Constructor and other methods...
    
    @Override
    public User save(User user) {
        try {
            // Existing implementation...
            datastore.put(entity);
        } catch (DatastoreException datastoreException) {
            String errorMessage = messageSource.getMessage(
                    "persistence.datastore.failed_to_save",
                    USER_ENTITY_ARG,
                    LocaleContextHolder.getLocale());
            throw new DatastorePersistenceException(errorMessage, datastoreException);
        }
        return user;
    }
    
    // Other methods...
}
catch block achieved.The code
private static final Object[] USER_ENTITY_ARG = new Object[]{User.KIND};
I do not like it for its' cons.
MessageHelper class with encapsulated Spring's MessageSource and pre-allocated memory for Object[] string interpolation arraysHere is the helper class:
@Service
public class MessageHelperImpl implements MessageHelper { // MessageHelper is just my interface you can see @Override's of its methods
    private static final ThreadLocal<Object[]> ARGS1 = ThreadLocal.withInitial(() -> new Object[1]);
    private static final ThreadLocal<Object[]> ARGS2 = ThreadLocal.withInitial(() -> new Object[2]);
    private final MessageSource messageSource;
    @Autowired
    public MessageHelperImpl(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
    ... 
    @Override
    public String getMessage(String code, Object arg1) {
        Object[] args = ARGS1.get();
        args[0] = arg1;
        return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
    }
    @Override
    public String getMessage(String code, Object arg1, Object arg2) {
        Object[] args = ARGS2.get();
        args[0] = arg1;
        args[1] = arg2;
        return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
    }
    @Override
    public String getMessage(String code, Object... args) {
        // For varargs, we use the provided array
        return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
    }
}
then the catch block looks like this:
        } catch (DatastoreException datastoreException) {
            // Using the specialized single-argument method
            String errorMessage = messageHelper.getMessage(
                    "persistence.datastore.failed_to_save", User.KIND); // User.KIND is just a String containing "User"
            throw new DatastorePersistenceException(errorMessage, datastoreException);
        }
messages.properties and string interpolation logicThreadLocal ARGS1 and ARGS2 buffers will be allocated before the actual call from the exception. So it does not solve my concern.ThreadLocal fields itself are created when the MessageHelperImpl class is loaded - Object[] arrays created lazily.
ARGS1.get() that call can be right from the exceptionsupplier () -> new Object[1]I do not like this solution as it does not guarantee to solve my concern - not to allocate new memory in catch block.
I see that potentially I can call the ARGS1.get() for each thread on initialization of the thread but it looks sooo messy like a very poor workaround.
At this moment I do not have working solution that is architecturally nice for the problem of having string interpolation for exception messages.
Please share your thoughts.
P.S. And I am still wondered whether this is normal to create a new exception in the catch block - while it is an existing practice of abstracting your app's exception processing from the "vendor-specific" exceptions it still allocates memory in catch block what is considered a bad practice.