A free instructor-led training on building Spring Boot applications backed by a GridGain / Apache Ignite cluster using Spring Data repositories and the thin client. Check the complete schedule and join an upcoming session.
During the live training you build a RESTful web service on top of a three-node GG8 cluster: Spring Data repositories for Country and City, a @Query SQL join for top-N most-populated cities, and a single REST endpoint that exercises the full stack.
- Prerequisites
- Project Layout
- 1. Clone the Project
- 2. Start the Cluster
- 3. Load the World Database
- 4. Configure Spring Boot and the Thin Client
- 5. Build and Run
- 6. Auto-Generated Repository Queries
- 7. Direct Queries With SQL Joins
- 8. REST Controller
- Shutdown
- Troubleshooting
- Git
- Docker Desktop
- A terminal — PowerShell on Windows, or any macOS / Linux terminal. Git Bash also works (see Troubleshooting for an MSYS path caveat)
- Your favorite IDE (IntelliJ, Eclipse, VS Code, or a plain editor)
- An HTTP client for verifying endpoints —
curl, Postman, or a browser all work
JDK 17 and Maven are optional — the app sidecar provides both. Install JDK 17 locally only if you use the standalone paths.
Linux only: the GridGain container image runs as UID 10000. If nodes fail to start on Linux, run chown -R 10000:10000 docker/data/ and retry.
Three GridGain nodes (node1, node2, node3) run on an isolated Docker bridge network. Only node1 publishes port 10800 to the host — that is the thin-client address the app connects to. The app service is a Maven 3.9 + JDK 17 sidecar: it shares the project directory via a bind mount, so you can build and run the app without installing Maven locally. The build writes only to libs/ — a directory the server nodes do not mount — so the cluster can stay up during sidecar builds.
config/
world.sql ← schema and data loaded into the cluster
docker/
docker-compose.yaml ← full topology rationale and mount details live here
config/ ← training-node-config.xml + ignite-log4j2.xml,
│ bind-mounted read-only into every server node
data/
│ node1/log/ ← node1 log files on the host (also via `docker compose logs node1`)
│ node2/log/ ← node2 log files
│ node3/log/ ← node3 log files
libs/ ← app.jar lands here after a build
src/ ← training source — edit these for the exercises
git clone https://github.com/GridGain-Demos/spring-data-training.git
cd spring-data-trainingYou should have received a license key a day or two before this session. Check your spam folder if you have not seen it yet. If you registered at the last minute, we'll share the key in the training, or you can download a key from our website.
Copy your license key to the docker folder. Ensure it's called gridgain-license.xml.
docker compose -f docker/docker-compose.yaml up -dVerify all three nodes joined:
Bash:
docker compose -f docker/docker-compose.yaml logs node1 | grep "Topology snapshot" | tail -1PowerShell:
docker compose -f docker/docker-compose.yaml logs node1 | Select-String "Topology snapshot" | Select-Object -Last 1Expect servers=3 in the output.
Edit config/world.sql. On the CREATE TABLE Country statement, add VALUE_TYPE inside the WITH clause:
) WITH "template=partitioned, backups=1, CACHE_NAME=Country, VALUE_TYPE=com.gridgain.training.spring.model.Country";On the CREATE TABLE City statement, add both VALUE_TYPE and KEY_TYPE:
) WITH "template=partitioned, backups=1, affinityKey=CountryCode, CACHE_NAME=City, VALUE_TYPE=com.gridgain.training.spring.model.City, KEY_TYPE=com.gridgain.training.spring.model.CityKey";These bindings make Ignite's binary metadata point at the Java model classes so the thin client can round-trip query results into Country and City objects.
Bash:
docker compose -f docker/docker-compose.yaml exec -T node1 /opt/gridgain/bin/sqlline.sh -u "jdbc:ignite:thin://127.0.0.1/" --silent=true < config/world.sqlPowerShell:
cmd /c "docker compose -f docker/docker-compose.yaml exec -T node1 /opt/gridgain/bin/sqlline.sh -u ""jdbc:ignite:thin://127.0.0.1/"" --silent=true < config/world.sql"Verify row counts:
Bash:
printf 'SELECT COUNT(*) FROM Country;\nSELECT COUNT(*) FROM City;\n!quit\n' | docker compose -f docker/docker-compose.yaml exec -T node1 /opt/gridgain/bin/sqlline.sh -u "jdbc:ignite:thin://127.0.0.1/" --silent=truePowerShell:
"SELECT COUNT(*) FROM Country;", "SELECT COUNT(*) FROM City;", "!quit" | Out-File -Encoding ascii verify.sql
cmd /c "docker compose -f docker/docker-compose.yaml exec -T node1 /opt/gridgain/bin/sqlline.sh -u ""jdbc:ignite:thin://127.0.0.1/"" --silent=true < verify.sql"
Remove-Item verify.sqlExpect 239 countries and 4079 cities.
Add these entries to the <dependencies> block. The extensions provide the IgniteRepository interface backed by an auto-configured thin client:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-spring-data-ext</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-spring-boot-thin-client-autoconfigure-ext</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<!--
Workaround: ignite-spring-data-ext:3.1.0 uses classes from
org.springframework.dao.* but does not declare spring-tx as a
transitive dep. Upstream bug; remove this entry once fixed.
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>The skeleton already declares org.gridgain:ignite-core:8.9.35 — leave that alone.
Edit src/main/java/com/gridgain/training/spring/Application.java and add @EnableIgniteRepositories:
@SpringBootApplication
@EnableIgniteRepositories
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Edit src/main/resources/application.properties:
ignite-client.addresses=${IGNITE_ADDRESS:localhost:10800}The ${IGNITE_ADDRESS:localhost:10800} default lets the same jar run from the host (localhost:10800) and from the docker app sidecar (IGNITE_ADDRESS=node1:10800 is baked into the compose service). The thin-client bean is auto-configured by the autoconfigure extension — no IgniteConfig class needed.
Demonstrate the Spring Boot integration by adding the following code to the Application.java class:
@Autowired
private IgniteClient ignite;
private Logger log = LoggerFactory.getLogger(Application.class);
@EventListener(ApplicationReadyEvent.class)
public void startupLogger() {
log.info("Table names existing in cluster: {}", ignite.cacheNames());
log.info("Node information:");
for (var n : ignite.cluster().nodes()) {
log.info("ID: {}, Version: {}", n.id(), n.version());
}
}Two paths — pick whichever suits your environment. The sidecar path requires no local SDK; the standalone path gives you IDE debugging and faster iteration.
Standalone (host Maven):
The cluster can stay up during a host Maven build.
mvn clean package -DskipTestsDocker:
The cluster can stay up — the build writes to libs/, which the server nodes do not mount.
docker compose -f docker/docker-compose.yaml run --rm app mvn -B clean package -DskipTestsBoth paths produce libs/app.jar.
Wait approximately 15 seconds after starting for the Started Application in … log line.
Standalone:
java @src/main/resources/j17.params -jar libs/app.jar --server.port=18080PowerShell (standalone):
Start-Process java -ArgumentList '@src/main/resources/j17.params', '-jar', 'libs/app.jar', '--server.port=18080'Docker:
docker compose -f docker/docker-compose.yaml run --rm -p 18080:18080 --name sd-app app java @/work/src/main/resources/j17.params -jar /work/libs/app.jar --server.port=18080PowerShell (Docker):
Start-Process docker -ArgumentList 'compose', '-f', 'docker/docker-compose.yaml', 'run', '--rm', '-p', '18080:18080', '--name', 'sd-app', 'app', 'java', '@/work/src/main/resources/j17.params', '-jar', '/work/libs/app.jar', '--server.port=18080'IGNITE_ADDRESS=node1:10800 is baked into the compose service so the app container reaches the cluster automatically.
Standalone:
^CPowerShell (standalone):
Stop-Process -Name java -ForceDocker:
^CCreate src/main/java/com/gridgain/training/spring/CountryRepository.java:
package com.gridgain.training.spring;
import java.util.List;
import com.gridgain.training.spring.model.Country;
import org.apache.ignite.springdata.repository.IgniteRepository;
import org.apache.ignite.springdata.repository.config.RepositoryConfig;
import org.springframework.stereotype.Repository;
@RepositoryConfig(cacheName = "Country")
@Repository
public interface CountryRepository extends IgniteRepository<Country, String> {
List<Country> findByPopulationGreaterThanOrderByPopulationDesc(int population);
}Add a test in src/test/java/com/gridgain/training/spring/ApplicationTests.java:
@Autowired CountryRepository countryRepository;
@Test
void countryRepositoryWorks() {
var results = countryRepository.findByPopulationGreaterThanOrderByPopulationDesc(100_000_000).size();
Assertions.assertTrue(results > 0);
}Run the tests by building the project again, as described in Section 5 above.
Create src/main/java/com/gridgain/training/spring/CityRepository.java:
package com.gridgain.training.spring;
import java.util.List;
import javax.cache.Cache;
import com.gridgain.training.spring.model.City;
import com.gridgain.training.spring.model.CityKey;
import org.apache.ignite.springdata.repository.IgniteRepository;
import org.apache.ignite.springdata.repository.config.Query;
import org.apache.ignite.springdata.repository.config.RepositoryConfig;
import org.springframework.stereotype.Repository;
@RepositoryConfig(cacheName = "City")
@Repository
public interface CityRepository extends IgniteRepository<City, CityKey> {
Cache.Entry<CityKey, City> findById(int id);
@Query("SELECT city.name, MAX(city.population), country.name FROM country " +
"JOIN city ON city.countrycode = country.code " +
"GROUP BY city.name, country.name, city.population " +
"ORDER BY city.population DESC LIMIT ?")
List<List<?>> findTopXMostPopulatedCities(int limit);
}Extend the test:
@Autowired CityRepository cityRepository;
@Test
void cityRepositoryWorks() {
var city = cityRepository.findById(34);
Assertions.assertEquals("Tirana", city.getValue().getName());
var populatedCities = cityRepository.findTopXMostPopulatedCities(5);
Assertions.assertEquals(5, populatedCities.size());
Assertions.assertEquals("Mumbai (Bombay)", populatedCities.get(0).get(0));
}Run the tests by building the project again, as described in Section 5 above.
Create src/main/java/com/gridgain/training/spring/WorldDatabaseController.java:
package com.gridgain.training.spring;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class WorldDatabaseController {
@Autowired
CityRepository cityRepository;
@GetMapping("/api/mostPopulated")
public List<List<?>> getMostPopulatedCities(@RequestParam("limit") int limit) {
return cityRepository.findTopXMostPopulatedCities(limit);
}
}Follow the steps in Section 5 to rebuild and start your Spring Boot server
Bash:
curl -s -w "HTTP %{http_code}\n" "http://localhost:18080/api/mostPopulated?limit=5"PowerShell:
curl.exe -s "http://localhost:18080/api/mostPopulated?limit=5"Expect [["Mumbai (Bombay)",10500000,"India"],["Seoul",...],...] with HTTP 200.
docker compose -f docker/docker-compose.yaml downThe docker/data/ directory is kept on the host (holds logs and marshaller metadata). Because persistence is not enabled in this training, there are no db/ or wal/ subdirectories.
| Symptom | Cause | Fix |
|---|---|---|
docker compose -f docker/docker-compose.yaml up -d hangs on the second attempt |
Port 10800 still held by a cluster running in another directory | docker compose -f docker/docker-compose.yaml down in that directory first |
Nodes start but produce no logs; docker/data/ empty (Linux only) |
Container runs as UID 10000; host docker/data/ owned by your user |
chown -R 10000:10000 docker/data/ |
Web server failed to start. Port 8080 was already in use. |
Something on the host owns port 8080 | Pass --server.port=18080 (already in the commands above) |
InaccessibleObjectException: Unable to make field long java.nio.Buffer.address accessible |
Missing @src/main/resources/j17.params before -jar |
Add the @ argfile argument |
Sidecar: Connection refused to thin client |
IGNITE_ADDRESS env var not set |
Check environment: block in docker/docker-compose.yaml sets IGNITE_ADDRESS=node1:10800 |
NoClassDefFoundError: InvalidDataAccessApiUsageException |
ignite-spring-data-ext:3.1.0 omits spring-tx as a transitive dependency |
Add spring-tx explicitly to pom.xml (see step 3.1) |