Database · Lesson 53 of 95
H2 In-Memory Database
Learn the H2 in-memory database in Spring Boot 4: automatic setup, data.sql seeding, file mode and the H2 console, with a runnable cafe menu app.
When a cafe owner sketches a new menu, she does not print it on glossy paper. She writes it on a whiteboard first. Changing a price takes one wipe, and nothing is wasted. When she is happy, she orders the real printed menu. An in-memory database plays the whiteboard role for a developer. It is fast, it needs no setup, and it disappears when you are done.
The H2 in-memory database is the most popular choice for this job in Java. In this topic you will learn what it is, where it fits, how to fill it with starting data, and how to open its web console.
What is the H2 in-memory database?
H2 is a small, fast relational database written in Java. It runs inside your application's own process. There is nothing to install and no server to start. In "in-memory mode", H2 keeps all tables in RAM, so everything is gone the moment the app stops.
H2 can also work in "file mode", where it saves data to a file on disk, so it survives a restart. But most Spring Boot developers use the in-memory mode for learning, demos and tests.
Why is it used?
- Zero setup. Add one dependency and the database exists. A new teammate can run the project in minutes.
- Fast tests. Tests start with a clean database every time, so they do not depend on each other.
- Safe practice. You cannot damage real data, because there is none.
- Quick prototypes. You can shape your entities first and choose the real database later.
But H2 is not meant for production. When the app restarts, the data vanishes. Its SQL also differs slightly from MySQL and PostgreSQL, so a query that works on H2 may fail on the real database. Use H2 for learning and tests, and use a server database for real users.
How it works
Spring Boot notices H2 and builds everything around it.
texth2 jar found on classpath | v No datasource URL set? | v Spring Boot starts embedded H2 | v Hibernate creates the tables | v data.sql adds starting rows | v App runs; data lives in RAM
Because there is no URL, Spring Boot starts an embedded H2 database on its own and gives it a user called sa. Hibernate then creates your tables from the entities. If a data.sql file exists, its statements run next. When the app stops, the whole database disappears with it.
Now see how you can look inside while the app runs.
textBrowser: /h2-console | v H2 console page (in your app) | v Login with the same JDBC URL | v Read tables, run SQL
The H2 console is a small web page that lives inside your own app. You open it in a browser, log in with the same JDBC URL your app uses, and you can browse tables and run SQL. It is a handy microscope for development.
Real-Life Example
Think about a driving school. Before students drive on real roads, they practise on a simulator. The simulator looks like a road and reacts like one, so students learn the controls. But if they crash, nobody is hurt, and the simulator resets in seconds. H2 is your simulator. You can crash it, reset it and try again. Just remember that a simulator is not a real road, so you still do a final test on the real database before your users arrive.
Code Example
Let's build a menu service for a cafe called Brew Corner. The app uses H2, fills the menu from a data.sql file, shows it at /menu, and switches on the H2 console.
textbrew-menu/ ├─ pom.xml └─ src/main/ ├─ java/com/brewcorner/menu/ │ ├─ MenuApplication.java │ ├─ MenuItem.java │ ├─ MenuItemRepository.java │ └─ MenuController.java └─ resources/ ├─ application.properties └─ data.sql
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.brewcorner</groupId> <artifactId>menu</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-h2console</artifactId> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Spring Boot 4 moved the console into its own library, spring-boot-h2console, so it must be added on purpose.
File: MenuApplication.java in package com.brewcorner.menu
javapackage com.brewcorner.menu; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MenuApplication { public static void main(String[] args) { SpringApplication.run(MenuApplication.class, args); } }
File: MenuItem.java in package com.brewcorner.menu
javapackage com.brewcorner.menu; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class MenuItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int price; protected MenuItem() { } public Long getId() { return id; } public String getName() { return name; } public int getPrice() { return price; } }
File: MenuItemRepository.java in package com.brewcorner.menu
javapackage com.brewcorner.menu; import org.springframework.data.jpa.repository.JpaRepository; public interface MenuItemRepository extends JpaRepository<MenuItem, Long> { }
File: MenuController.java in package com.brewcorner.menu
javapackage com.brewcorner.menu; import java.util.List; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MenuController { private final MenuItemRepository repository; public MenuController(MenuItemRepository repository) { this.repository = repository; } @GetMapping("/menu") public List<MenuItem> menu() { return repository.findAll(); } }
File: application.properties in src/main/resources
propertiesspring.application.name=brew-menu spring.datasource.url=jdbc:h2:mem:cafe spring.datasource.username=sa spring.jpa.defer-datasource-initialization=true spring.h2.console.enabled=true
File: data.sql in src/main/resources
sqlINSERT INTO menu_item (name, price) VALUES ('Filter Coffee', 60); INSERT INTO menu_item (name, price) VALUES ('Masala Chai', 30); INSERT INTO menu_item (name, price) VALUES ('Veg Sandwich', 90);
Start the app, then call the menu from a second terminal:
bashmvn spring-boot:run curl http://localhost:8080/menu
Output:
json[ { "id": 1, "name": "Filter Coffee", "price": 60 }, { "id": 2, "name": "Masala Chai", "price": 30 }, { "id": 3, "name": "Veg Sandwich", "price": 90 } ]
The reply comes back on one line. It is spaced out here so it is easy to read. Open the /h2-console path of your running app in a browser, enter the JDBC URL jdbc:h2:mem:cafe and user sa with an empty password, and you can run SELECT * FROM menu_item.
Code Explained
- The
h2dependency has runtime scope. Spring Boot needs it on the classpath, and your code never imports it. - The datasource URL
jdbc:h2:mem:cafenames the in-memory databasecafe. A fixed name lets you type the same URL into the console. - The deferred-initialization property in the
spring.jpagroup tells Spring Boot to rundata.sqlafter Hibernate has created the tables. Without it, the inserts run first and fail, because the table does not exist yet. data.sqlsits insrc/main/resources, and Spring Boot runs it at every startup.spring.h2.console.enabled=trueswitches on the web console, andspring-boot-h2consolesupplies it.- Hibernate creates the table
menu_itemfrom the entity, because an embedded database getscreate-dropby default.
Choosing an H2 URL
| Mode | URL | Data after restart |
|---|---|---|
| In memory | jdbc:h2:mem:cafe | Lost |
| File on disk | jdbc:h2:file:./data/cafe | Kept |
| In memory, MySQL style | jdbc:h2:mem:cafe;MODE=MySQL | Lost |
Common Mistakes
- `data.sql` fails with "table not found". Turn on the deferred-initialization property in the
spring.jpagroup, so Hibernate builds the tables first. - Console cannot find the database. The URL in the login page must be exactly the URL in your properties. With a random name, copy it from the startup log.
- Trusting H2 as a full copy of production. Test important queries on the real database too, because the SQL dialects differ.
- Console page missing. In Spring Boot 4, the
spring-boot-h2consolelibrary must be on the classpath, along with the web starter.
Interview Questions
What is an in-memory database, and when do you use one?
Ans:It keeps data in RAM and loses it on shutdown. Developers use it for learning, demos and fast automated tests.
What does Spring Boot do when H2 is on the classpath and no URL is given?
Ans:It starts an embedded H2 database, creates a default user sa, and lets Hibernate create the tables.
Why does `data.sql` sometimes fail?
Ans:It can run before Hibernate creates the tables. Turning on the deferred-initialization property in the spring.jpa group fixes the order.
Key Points to Remember
- H2 is a Java database that runs inside your app. In memory mode it forgets everything at shutdown.
- Spring Boot configures an embedded H2 by itself when no datasource URL is set.
data.sqladds starting rows, and it needs the deferred-initialization property when Hibernate builds the tables.- The console needs the
spring-boot-h2consolelibrary in Spring Boot 4 and a property to enable it. - H2 suits practice and tests. Use MySQL or PostgreSQL for production.
Frequently Asked Questions
Why does my data disappear from the H2 in-memory database on restart?
That is how in-memory mode works. To keep data, use a file URL like jdbc:h2:file:./data/cafe, or move to a real database server.
Can I use H2 for tests and MySQL for the real app?
Yes, and many teams do. Keep the H2 driver on the test scope, but remember to check key queries against the real database as well.
What is the default H2 username and password?
The user is sa and the password is empty. That is fine for a throwaway local database and never for anything real.
Is H2 the same as SQLite?
No. Both are small databases, but H2 is written in Java and runs inside your JVM, while SQLite is a C library that stores data in one file.
Related Topics
- Connecting Spring Boot to MySQL: move from H2 to a real server.
- JPA Entity: shape the classes that become tables.
- Data JPA Test: use an in-memory database in fast repository tests.
- Spring Profiles: switch databases per environment.
Practice Problems
Try each problem on your own first. Each project has its own pom.xml, shown below.
Easy: Film Club Seeded from data.sql
Riverside Film Club wants a small H2 database that starts with three films already inside. Build an app with an entity Film (a title and a year), a data.sql file with three inserts, and a runner that prints how many films exist and then their titles.
Use these films: Paper Boats (2019), Night Market (2021), The Last Ferry (2023).
Show answerHide answer
data.sql fills it. The runner only reads. It does not save anything.File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.riverside</groupId> <artifactId>filmclub</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.properties in src/main/resources
propertiesspring.application.name=filmclub spring.jpa.defer-datasource-initialization=true
File: data.sql in src/main/resources
sqlINSERT INTO film (title, release_year) VALUES ('Paper Boats', 2019); INSERT INTO film (title, release_year) VALUES ('Night Market', 2021); INSERT INTO film (title, release_year) VALUES ('The Last Ferry', 2023);
File: FilmClubApplication.java in package com.riverside.filmclub
javapackage com.riverside.filmclub; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class FilmClubApplication { public static void main(String[] args) { SpringApplication.run(FilmClubApplication.class, args); } }
File: Film.java in package com.riverside.filmclub
javapackage com.riverside.filmclub; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class Film { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private int releaseYear; protected Film() { } public String getTitle() { return title; } }
File: FilmRepository.java in package com.riverside.filmclub
javapackage com.riverside.filmclub; import org.springframework.data.jpa.repository.JpaRepository; public interface FilmRepository extends JpaRepository<Film, Long> { }
File: FilmRunner.java in package com.riverside.filmclub
javapackage com.riverside.filmclub; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class FilmRunner implements CommandLineRunner { private final FilmRepository repository; public FilmRunner(FilmRepository repository) { this.repository = repository; } @Override public void run(String... args) { System.out.println("Films in database: " + repository.count()); repository.findAll().forEach(f -> System.out.println("- " + f.getTitle())); } }
The output is:
textFilms in database: 3 - Paper Boats - Night Market - The Last Ferry
Medium: Visitor Log that Survives a Restart
The reception desk at Green Valley Hospital keeps a visitor log. Use H2 in file mode so the log survives a restart. Each time the app starts, it saves one new visit and prints how many visits the file holds in total. Run the app twice to prove that the data stayed.
Show answerHide answer
data folder next to where you run the app. update keeps the existing table, so the count grows by one on each run. Delete the data folder to start again.File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.greenvalley</groupId> <artifactId>visitorlog</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.properties in src/main/resources
propertiesspring.application.name=visitorlog spring.datasource.url=jdbc:h2:file:./data/visitors spring.datasource.username=sa spring.jpa.hibernate.ddl-auto=update
File: VisitorApplication.java in package com.greenvalley.visitorlog
javapackage com.greenvalley.visitorlog; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class VisitorApplication { public static void main(String[] args) { SpringApplication.run(VisitorApplication.class, args); } }
File: Visit.java in package com.greenvalley.visitorlog
javapackage com.greenvalley.visitorlog; import java.time.LocalDate; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class Visit { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private LocalDate visitDate; protected Visit() { } public Visit(LocalDate visitDate) { this.visitDate = visitDate; } }
File: VisitRepository.java in package com.greenvalley.visitorlog
javapackage com.greenvalley.visitorlog; import org.springframework.data.jpa.repository.JpaRepository; public interface VisitRepository extends JpaRepository<Visit, Long> { }
File: VisitRunner.java in package com.greenvalley.visitorlog
javapackage com.greenvalley.visitorlog; import java.time.LocalDate; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class VisitRunner implements CommandLineRunner { private final VisitRepository repository; public VisitRunner(VisitRepository repository) { this.repository = repository; } @Override public void run(String... args) { repository.save(new Visit(LocalDate.now())); System.out.println("Visits so far: " + repository.count()); } }
Run mvn spring-boot:run twice. The first run prints Visits so far: 1, and the second run prints:
textVisits so far: 2