diff --git a/java/IceGrid/query/.gitignore b/java/IceGrid/query/.gitignore
new file mode 100644
index 0000000000..694c577cdd
--- /dev/null
+++ b/java/IceGrid/query/.gitignore
@@ -0,0 +1,4 @@
+# Gradle generated build files
+bin
+build
+.gradle
diff --git a/java/IceGrid/query/README.md b/java/IceGrid/query/README.md
new file mode 100644
index 0000000000..4bcbec6cde
--- /dev/null
+++ b/java/IceGrid/query/README.md
@@ -0,0 +1,55 @@
+# IceGrid Query
+
+This demo shows how to use the Query object provided by the IceGrid registry to lookup a well-known object by type.
+
+## Ice prerequisites
+
+- Install IceGrid. See [Ice service installation].
+
+## Building the demo
+
+The demo has two Gradle projects, **client** and **server**, both using the [application plugin].
+
+To build the demo, run:
+
+```shell
+./gradlew installDist
+```
+
+This creates a self-contained distribution under build/install/ for each application.
+Each distribution includes:
+
+- a bin/ directory with start scripts, and
+- a lib/ directory containing the application JAR and all its runtime dependencies.
+
+In our IceGrid deployment, we call java directly and set the class path to include everything in the distribution’s lib/
+directory. This ensures both the server JAR and its dependencies (such as Ice) are available at runtime.
+
+## Running the demo
+
+First, start the IceGrid registry in its own terminal:
+
+```shell
+icegridregistry --Ice.Config=config.registry
+```
+
+Then, start the IceGrid node in its own terminal:
+
+```shell
+icegridnode --Ice.Config=config.node
+```
+
+Next, deploy the "GreeterHall" application in this IceGrid deployment:
+
+```shell
+icegridadmin --Ice.Config=config.admin -e "application add greeter-hall.xml"
+```
+
+Finally, run the client application:
+
+```shell
+./gradlew :client:run --quiet
+```
+
+[Application plugin]: https://docs.gradle.org/current/userguide/application_plugin.html
+[Ice service installation]: https://github.com/zeroc-ice/ice/blob/main/NIGHTLY.md#ice-services
diff --git a/java/IceGrid/query/client/build.gradle.kts b/java/IceGrid/query/client/build.gradle.kts
new file mode 100644
index 0000000000..5ce09b3165
--- /dev/null
+++ b/java/IceGrid/query/client/build.gradle.kts
@@ -0,0 +1,32 @@
+// Copyright (c) ZeroC, Inc.
+
+plugins {
+    // Apply the application plugin to tell gradle this is a runnable Java application.
+    id("application")
+
+    // Apply the Slice-tools plugin to enable Slice compilation.
+    id("com.zeroc.slice-tools") version "3.8.+"
+
+    // Pull in our local 'convention plugin' to enable linting.
+    id("zeroc-linting")
+}
+
+dependencies {
+    // Add the Ice and IceGrid libraries as an implementation dependencies.
+    implementation("com.zeroc:ice:3.8.+")
+    implementation("com.zeroc:icegrid:3.8.+")
+}
+
+sourceSets {
+    main {
+        // Add the Greeter.ice file from the parent slice directory to the main source set.
+        slice {
+            srcDirs("../slice")
+        }
+    }
+}
+
+application {
+    // Specify the main entry point for the application.
+    mainClass.set("com.example.icegrid.query.client.Client")
+}
diff --git a/java/IceGrid/query/client/src/main/java/com/example/icegrid/query/client/Client.java b/java/IceGrid/query/client/src/main/java/com/example/icegrid/query/client/Client.java
new file mode 100644
index 0000000000..b79db6d53e
--- /dev/null
+++ b/java/IceGrid/query/client/src/main/java/com/example/icegrid/query/client/Client.java
@@ -0,0 +1,49 @@
+// Copyright (c) ZeroC, Inc.
+
+package com.example.icegrid.query.client;
+
+import com.example.visitorcenter.GreeterPrx;
+import com.zeroc.Ice.Communicator;
+import com.zeroc.Ice.LocatorPrx;
+import com.zeroc.Ice.ObjectPrx;
+import com.zeroc.Ice.Util;
+import com.zeroc.IceGrid.QueryPrx;
+
+class Client {
+    public static void main(String[] args) {
+        // Create an Ice communicator. We'll use this communicator to create proxies and manage outgoing connections.
+        try (Communicator communicator = Util.initialize(args)) {
+
+            // Set the default locator of the new communicator. It's the address of the Locator hosted by our IceGrid
+            // registry. You can also set this proxy with the Ice.Default.Locator property.
+            communicator.setDefaultLocator(
+                LocatorPrx.createProxy(communicator, "IceGrid/Locator:tcp -h localhost -p 4061"));
+
+            // Create a proxy to the Query object hosted by the IceGrid registry. "IceGrid/Query" a well-known proxy,
+            // without addressing information.
+            QueryPrx query = QueryPrx.createProxy(communicator, "IceGrid/Query");
+
+            // Look up an object with type ::VisitorCenter::Greeter.
+            String greeterTypeId = GreeterPrx.ice_staticId(); // ::VisitorCenter::Greeter
+            ObjectPrx proxy = query.findObjectByType(greeterTypeId);
+
+            if (proxy == null) {
+                System.out.println(
+                    String.format("The IceGrid registry doesn't know any object with type '%s'.", greeterTypeId));
+            } else {
+                // Cast the object proxy to a Greeter proxy.
+                GreeterPrx greeter = GreeterPrx.uncheckedCast(proxy);
+
+                // Send a request to the remote object and get the response.
+                String greeting = greeter.greet(System.getProperty("user.name"));
+                System.out.println(greeting);
+
+                // Send another request to the remote object. With the default configuration we use for this client,
+                // this request reuses the connection and reaches the same server, even when we have multiple
+                // replicated servers.
+                greeting = greeter.greet("alice");
+                System.out.println(greeting);
+            }
+        }
+    }
+}
diff --git a/java/IceGrid/query/config.admin b/java/IceGrid/query/config.admin
new file mode 100644
index 0000000000..e84a9f2b72
--- /dev/null
+++ b/java/IceGrid/query/config.admin
@@ -0,0 +1,8 @@
+# Config file for icegridadmin
+
+# A proxy to the Locator object hosted by the IceGrid registry.
+Ice.Default.Locator=IceGrid/Locator:tcp -h localhost -p 4061
+
+# Dummy username and password. They work because IceGrid is configured with the NullPermissionsVerifier.
+IceGridAdmin.Username=foo
+IceGridAdmin.Password=bar
diff --git a/java/IceGrid/query/config.node b/java/IceGrid/query/config.node
new file mode 100644
index 0000000000..e234e4afca
--- /dev/null
+++ b/java/IceGrid/query/config.node
@@ -0,0 +1,18 @@
+# Config file for icegridnode
+
+# A proxy to the Locator object hosted by the IceGrid registry.
+Ice.Default.Locator=IceGrid/Locator:tcp -h localhost -p 4061
+
+# The name of this IceGrid node.
+IceGrid.Node.Name=node1
+
+# The endpoints of this node's object adapter. This object adapter receives requests from the IceGrid registry.
+# We configure this object adapter to listen on an OS-assigned tcp port on the loopback interface since the IceGrid
+# registry runs on the same host in this deployment.
+IceGrid.Node.Endpoints=tcp -h 127.0.0.1
+
+# The directory where the node stores the config files for the Ice servers it starts.
+IceGrid.Node.Data=db/node
+
+# Trace activation of Ice servers (3 = very verbose).
+IceGrid.Node.Trace.Activator=3
diff --git a/java/IceGrid/query/config.registry b/java/IceGrid/query/config.registry
new file mode 100644
index 0000000000..65435349ca
--- /dev/null
+++ b/java/IceGrid/query/config.registry
@@ -0,0 +1,22 @@
+# Config file for icegridregistry
+
+# The endpoints of this registry's client object adapter. This object adapter receives requests from the clients.
+# We configure it to listen on tcp port 4061, on all interfaces.
+IceGrid.Registry.Client.Endpoints=tcp -p 4061
+
+# The endpoints of this registry's server object adapter. This object adapter receives requests from the servers when
+# they register/unregister themselves or their endpoints with the registry.
+# We configure this object adapter to listen on an OS-assigned tcp port, on all interfaces.
+IceGrid.Registry.Server.Endpoints=tcp
+
+# The endpoints of this registry's internal object adapter. This object adapter receives requests from the IceGrid
+# nodes.
+# We configure this object adapter to listen on an OS-assigned tcp port, on the loopback interface since the IceGrid
+# node runs on the same host in this deployment.
+IceGrid.Registry.Internal.Endpoints=tcp -h 127.0.0.1
+
+# The directory when the registry stores its LMDB database. This directory must exist and be writable by the registry.
+IceGrid.Registry.LMDB.Path=db/registry
+
+# The admin interface of the registry accepts any username/password combination.
+IceGrid.Registry.AdminPermissionsVerifier=IceGrid/NullPermissionsVerifier
diff --git a/java/IceGrid/query/db/node/.gitignore b/java/IceGrid/query/db/node/.gitignore
new file mode 100644
index 0000000000..6f76220e99
--- /dev/null
+++ b/java/IceGrid/query/db/node/.gitignore
@@ -0,0 +1,2 @@
+servers/
+tmp/
diff --git a/java/IceGrid/query/db/registry/.gitignore b/java/IceGrid/query/db/registry/.gitignore
new file mode 100644
index 0000000000..fb24cad008
--- /dev/null
+++ b/java/IceGrid/query/db/registry/.gitignore
@@ -0,0 +1,2 @@
+*.mdb
+*.lock
diff --git a/java/IceGrid/query/gradle/wrapper/gradle-wrapper.jar b/java/IceGrid/query/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..a4b76b9530
Binary files /dev/null and b/java/IceGrid/query/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/java/IceGrid/query/gradle/wrapper/gradle-wrapper.properties b/java/IceGrid/query/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..d9fbee2e1d
--- /dev/null
+++ b/java/IceGrid/query/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-all.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/java/IceGrid/query/gradlew b/java/IceGrid/query/gradlew
new file mode 100755
index 0000000000..f3b75f3b0d
--- /dev/null
+++ b/java/IceGrid/query/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed 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
+#
+#      https://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.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/java/IceGrid/query/gradlew.bat b/java/IceGrid/query/gradlew.bat
new file mode 100755
index 0000000000..9d21a21834
--- /dev/null
+++ b/java/IceGrid/query/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/java/IceGrid/query/greeter-hall.xml b/java/IceGrid/query/greeter-hall.xml
new file mode 100644
index 0000000000..6f93d5adcd
--- /dev/null
+++ b/java/IceGrid/query/greeter-hall.xml
@@ -0,0 +1,42 @@
+
+  
+  
+
+    
+    
+      
+      
+
+      
+      
+        
+        
+        
+
+        
+        
+
+        
+        
+          
+          
+        
+      
+    
+
+    
+    
+      
+      
+      
+    
+  
+
diff --git a/java/IceGrid/query/server/build.gradle.kts b/java/IceGrid/query/server/build.gradle.kts
new file mode 100644
index 0000000000..4fee4cd418
--- /dev/null
+++ b/java/IceGrid/query/server/build.gradle.kts
@@ -0,0 +1,31 @@
+// Copyright (c) ZeroC, Inc.
+
+plugins {
+    // Apply the application plugin to tell gradle this is a runnable Java application.
+    id("application")
+
+    // Apply the Slice-tools plugin to enable Slice compilation.
+    id("com.zeroc.slice-tools") version "3.8.+"
+
+    // Pull in our local 'convention plugin' to enable linting.
+    id("zeroc-linting")
+}
+
+dependencies {
+    // Add the Ice library as an implementation dependency.
+    implementation("com.zeroc:ice:3.8.+")
+}
+
+sourceSets {
+    main {
+        // Add the Greeter.ice file from the parent slice directory to the main source set.
+        slice {
+            srcDirs("../slice")
+        }
+    }
+}
+
+application {
+    // Specify the main entry point for the application.
+    mainClass.set("com.example.icegrid.query.server.Server")
+}
diff --git a/java/IceGrid/query/server/src/main/java/com/example/icegrid/query/server/Chatbot.java b/java/IceGrid/query/server/src/main/java/com/example/icegrid/query/server/Chatbot.java
new file mode 100644
index 0000000000..bcdc5b4ee1
--- /dev/null
+++ b/java/IceGrid/query/server/src/main/java/com/example/icegrid/query/server/Chatbot.java
@@ -0,0 +1,24 @@
+// Copyright (c) ZeroC, Inc.
+
+package com.example.icegrid.query.server;
+
+import com.example.visitorcenter.Greeter;
+import com.zeroc.Ice.Current;
+
+/**
+ * Chatbot is an Ice servant that implements Slice interface Greeter.
+ */
+class Chatbot implements Greeter {
+    private final String _greeterName;
+
+    Chatbot(String greeterName) {
+        this._greeterName = greeterName;
+    }
+
+    // Implements the abstract method 'greet' from the Greeter interface generated by the Slice compiler.
+    @Override
+    public String greet(String name, Current current) {
+        System.out.println("Dispatching greet request { name = '" + name + "' }");
+        return "Hello, " + name + "! My name is " + _greeterName + ". How are you doing today?";
+    }
+}
diff --git a/java/IceGrid/query/server/src/main/java/com/example/icegrid/query/server/Server.java b/java/IceGrid/query/server/src/main/java/com/example/icegrid/query/server/Server.java
new file mode 100644
index 0000000000..5b3c6a05af
--- /dev/null
+++ b/java/IceGrid/query/server/src/main/java/com/example/icegrid/query/server/Server.java
@@ -0,0 +1,38 @@
+// Copyright (c) ZeroC, Inc.
+
+package com.example.icegrid.query.server;
+
+import com.zeroc.Ice.Communicator;
+import com.zeroc.Ice.Identity;
+import com.zeroc.Ice.ObjectAdapter;
+import com.zeroc.Ice.Properties;
+import com.zeroc.Ice.Util;
+
+class Server {
+    public static void main(String[] args) {
+        // Create an Ice communicator. We'll use this communicator to create an object adapter.
+        // IceGrid starts this server with --Ice.Config=, so it's essential to
+        // initialize this communicator with args.
+        try (Communicator communicator = Util.initialize(args)) {
+
+            // Create an object adapter. It's configured by the GreeterAdapter.* properties in the IceGrid-generated
+            // config file.
+            ObjectAdapter adapter = communicator.createObjectAdapter("GreeterAdapter");
+
+            // Retrieve the greeter name and greeter identity from the IceGrid-generated config file.
+            Properties properties = communicator.getProperties();
+            String greeterName = properties.getProperty("Ice.ProgramName");
+            Identity greeterIdentity = Util.stringToIdentity(properties.getProperty("Greeter.Identity"));
+
+            // Register the Chatbot servant with the adapter.
+            adapter.add(new Chatbot(greeterName), greeterIdentity);
+
+            // Start dispatching requests.
+            adapter.activate();
+            System.out.println(greeterName + " is listening...");
+
+            // Wait until the communicator is shut down. IceGrid shuts down this communicator via its Ice.Admin object.
+            communicator.waitForShutdown();
+        }
+    }
+}
diff --git a/java/IceGrid/query/settings.gradle.kts b/java/IceGrid/query/settings.gradle.kts
new file mode 100644
index 0000000000..3693a30cb8
--- /dev/null
+++ b/java/IceGrid/query/settings.gradle.kts
@@ -0,0 +1,34 @@
+// Copyright (c) ZeroC, Inc.
+
+pluginManagement {
+    repositories {
+        mavenLocal()
+        gradlePluginPortal()
+        // This demo uses the nightly build of the Slice Tools plugin, published to the ZeroC Maven Nightly repository.
+        maven {
+            url = uri("https://download.zeroc.com/nexus/repository/maven-nightly/")
+            content {
+                includeGroupByRegex("com\\.zeroc.*")
+            }
+        }
+    }
+}
+
+dependencyResolutionManagement {
+    repositories {
+        mavenCentral()
+        // This demo uses the nightly build of Ice, published to the ZeroC Maven Nightly repository.
+        maven {
+            url = uri("https://download.zeroc.com/nexus/repository/maven-nightly/")
+            content {
+                includeGroupByRegex("com\\.zeroc.*")
+            }
+        }
+    }
+}
+
+rootProject.name = "greeter"
+include("client")
+include("server")
+
+includeBuild("../../build-logic")
diff --git a/java/IceGrid/query/slice/Greeter.ice b/java/IceGrid/query/slice/Greeter.ice
new file mode 100644
index 0000000000..2ef57b32e8
--- /dev/null
+++ b/java/IceGrid/query/slice/Greeter.ice
@@ -0,0 +1,14 @@
+// Copyright (c) ZeroC, Inc.
+
+["java:identifier:com.example.visitorcenter"]
+module VisitorCenter
+{
+    /// Represents a simple greeter.
+    interface Greeter
+    {
+        /// Creates a personalized greeting.
+        /// @param name The name of the person to greet.
+        /// @return The greeting.
+        string greet(string name);
+    }
+}