The previous lesson closed module 3 with an uncomfortable observation: the CicloUrbana API is complete, documented and ready to publish, but when the process restarts everything disappears. Ribalta's four stations are born again out of DemoStationLoader, the bikes registered through the API evaporate and the day's rentals are lost. Everything lives in the ConcurrentHashMap inside InMemoryStationRepository, which exists for exactly as long as the JVM does. This module replaces that stopgap with real persistence on a relational database, and it does so without touching a single controller, because ever since 02-01 we have hidden storage behind the StationRepository interface.
Before writing a single annotation it pays to understand the terrain. This lesson is conceptual, and it answers the questions almost nobody asks before starting and everybody asks when something breaks: why an ORM exists, what exactly each of the four acronyms that get mixed up daily means —JDBC, JPA, Hibernate, Spring Data JPA—, what the persistence context is and why an entity can be "managed" or "detached", and what Spring Boot does on its own as soon as it detects the starter. Without this foundation JPA looks like magic; with it, it is a predictable machine.
Contents
- The object-relational impedance mismatch
- What an ORM is and what problem it really solves
- Who's who: JDBC, JPA, Hibernate and Spring Data JPA
- What
spring-boot-starter-data-jpabrings EntityManager, persistence unit and persistence context- The four states of an entity
- What Spring Boot generates when it detects the starter
- Alternatives to JPA in the Spring ecosystem
- CicloUrbana's target data model
- Common Mistakes and Tips
- Exercises
- The object-relational impedance mismatch
In Java, Station is an object: it has its own identity (two distinct objects even when they match field by field), direct references to other objects, inheritance, and it lives in a graph managed by the garbage collector. In a relational database a station is a row in a table: its identity comes from the primary key, it relates to other rows through foreign keys, it inherits from nothing and it only exists inside a set of tuples.
Both models are good, but they do not fit together. That collection of disagreements is called the object-relational impedance mismatch, and it has five concrete faces:
| Mismatch | In Java | In SQL | Practical consequence |
|---|---|---|---|
| Granularity | Station holds a Location object with latitude and longitude |
The stations table has two loose columns |
Objects must be flattened or expanded |
| Identity | == (reference) and equals() (value) are different things |
Only the primary key exists | Two objects can represent the same row |
| Inheritance | Incident → BatteryIncident, VandalismIncident |
There is no inheritance between tables | You have to pick a mapping strategy |
| Association | Directed reference: Bike.station |
Symmetric foreign key, navigable both ways | Bidirectionality has to be built by hand |
| Navigation | bike.getStation().getName(), hop by hop |
JOIN, everything at once |
Navigating object by object generates many queries |
An example from CicloUrbana makes it tangible. To get the name of a bike's station, the natural thing in Java is:
In SQL the natural thing is the opposite: ask for it all at once.
The first form, run inside a loop over a hundred bikes, produces a hundred queries. That is the seed of the N+1 problem we will see in 04-04. It is not a JPA defect: it is the mismatch showing through.
- What an ORM is and what problem it really solves
An ORM (Object-Relational Mapping) is a layer that translates automatically between objects and rows. You write stationRepository.save(station) and somebody generates the INSERT; you read station.getBikes() and somebody generates the SELECT.
To appreciate what it contributes, look at what doing it by hand with plain JDBC would cost. This would be the save for a station without an ORM:
public Station save(Station station) {
String sql = """
INSERT INTO stations (name, address, capacity, latitude, longitude)
VALUES (?, ?, ?, ?, ?)
""";
try (Connection connection = dataSource.getConnection();
PreparedStatement statement =
connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, station.getName());
statement.setString(2, station.getAddress());
statement.setInt(3, station.getCapacity());
statement.setDouble(4, station.getLatitude());
statement.setDouble(5, station.getLongitude());
statement.executeUpdate();
try (ResultSet keys = statement.getGeneratedKeys()) {
if (keys.next()) {
station.setId(keys.getLong(1));
}
}
return station;
} catch (SQLException e) {
throw new DataStoreException("Could not save the station", e);
}
}Twenty lines for a five-column table. Multiply that by findById, findAll, update, delete, and then by the six tables in the model. And we still owe the ResultSet → object conversion, the null handling, and the fact that every ALTER TABLE forces you to review all those hand-numbered parameter positions.
What an ORM contributes, ordered by real importance:
- It removes the repetitive mapping code. Eighty percent of that snippet disappears.
- It manages identity and state. It knows which objects it loaded and which ones changed, and it generates an
UPDATEfor the modified parts only. - It translates engine-specific SQL. The same code runs against H2 in development and PostgreSQL in production.
- It offers an object-oriented query language (JPQL) validated against the model rather than against strings.
- It automates the loading of associations and gives you control over when to load them.
And what an ORM does not contribute, because it is worth saying from the outset:
- It does not excuse you from knowing SQL. When something is slow, the diagnosis is made by reading the generated SQL.
- It does not turn a bad relational model into a good one.
- It is not free: it adds a layer with rules of its own that you have to understand (precisely what this module is about).
- Who's who: JDBC, JPA, Hibernate and Spring Data JPA
These four pieces get named daily as if they were interchangeable, and they are not. Each lives at a different level of abstraction and depends on the previous one.
| Piece | What it is | Who maintains it | Kind of artefact | Example of use |
|---|---|---|---|---|
| JDBC | Standard Java API for talking to a database via SQL | Java SE specification | API + one driver per engine | connection.prepareStatement(sql) |
| JPA (Jakarta Persistence) | Specification: it defines annotations, EntityManager, JPQL and the lifecycle. It executes nothing |
Jakarta EE (formerly JSR-338) | Interfaces and annotations | @Entity, EntityManager.persist() |
| Hibernate | Implementation of JPA: the engine that actually generates and runs the SQL | Red Hat | Library (hibernate-core) |
It is the one that writes the INSERT |
| Spring Data JPA | Abstraction on top of JPA: it generates repository implementations from interfaces | Spring (VMware/Broadcom) | Library (spring-data-jpa) |
interface StationRepository extends JpaRepository<...> |
The critical distinction is that JPA is a specification and Hibernate is an implementation. JPA states that @Entity exists and what it must mean; Hibernate writes the code that makes it mean that. Other implementations are EclipseLink and OpenJPA, but Spring Boot ships Hibernate by default and it is the de facto standard choice.
graph TD
A["StationService (your business code)"] --> B["StationRepository<br/>Spring Data JPA interface"]
B --> C["SimpleJpaRepository<br/>generated implementation"]
C --> D["EntityManager<br/>JPA API (specification)"]
D --> E["Hibernate<br/>JPA implementation"]
E --> F["JDBC + PostgreSQL driver"]
F --> G[("PostgreSQL 16")]
Every downward arrow is a translation. stationRepository.findById(1L) becomes a call to EntityManager.find(Station.class, 1L), which Hibernate turns into a SELECT ... WHERE id = ?, which the JDBC driver sends over the socket to PostgreSQL. When something goes wrong, the diagnosis consists of walking down these layers until you find the one where the translation broke.
One nuance that saves confusion: Spring Data JPA does not replace Hibernate, it leans on it. And Spring Data JPA is not Spring Data JDBC; they share a family and the repository style, but underneath they are different projects (we cover this in section 8).
- What
spring-boot-starter-data-jpa brings
spring-boot-starter-data-jpa bringsLike every starter (02-06), it is a POM with no code that pulls in a coherent set of dependencies whose versions are already compatible with each other.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>That single line adds to the classpath:
| Dependency | What it is for |
|---|---|
spring-boot-starter-jdbc |
DataSource, JdbcTemplate and the HikariCP pool |
hibernate-core |
The ORM engine: the JPA implementation |
jakarta.persistence-api |
The annotations and interfaces of the specification |
spring-data-jpa |
The repositories and their automatic generation |
spring-orm |
Spring's integration with JPA and its transaction manager |
spring-aspects |
AOP support, the basis of @Transactional (04-07) |
jakarta.transaction-api |
The transaction API |
What it does not include —deliberately— is the database driver, because the starter cannot know which engine you are going to connect to. Adding H2 or PostgreSQL is the first thing we will do in 04-02.
You can check the real tree in your project:
EntityManager, persistence unit and persistence context
EntityManager, persistence unit and persistence contextThese three terms are JPA's basic vocabulary. Confusing them is the cause of half the forum questions about "why aren't my changes being saved".
Persistence unit: the complete configuration of a data store —which DataSource to use, which classes are entities, which SQL dialect, which Hibernate properties—. In a classic JPA application it was declared in persistence.xml. In Spring Boot that file does not exist: the persistence unit is built from application.yml and from the automatic scanning of entities. It is one of the things the starter does for you.
EntityManager: JPA's central interface. It is your gateway to persistence. Its essential operations:
| Method | What it does |
|---|---|
persist(entity) |
Marks a new entity to be inserted |
find(Class, id) |
Retrieves by primary key (looks in the context first) |
getReference(Class, id) |
Returns a lazy proxy without hitting the database |
merge(entity) |
Reattaches a detached entity to the context |
remove(entity) |
Marks a managed entity for deletion |
flush() |
Forces the pending SQL to be sent to the database |
detach(entity) / clear() |
Takes one entity, or all of them, out of the context |
createQuery(jpql) |
Creates a JPQL query |
With Spring Data JPA you will almost never use it directly, because repositories encapsulate it. But it is down there, and understanding its behaviour explains the behaviour of repositories.
Persistence context: this is the concept you really have to internalise. It is a first-level cache that the EntityManager keeps with every entity it has loaded or saved during a unit of work. It has three properties that change everything:
- It guarantees unique identity: within the same context, two lookups of station 1 return exactly the same Java object.
stationA == stationBistrue. - It detects changes automatically (dirty checking): it keeps a copy of the original state; on flush it compares and generates an
UPDATEonly if something changed. That is why, on a managed entity, there is no need to callsave()(we will develop this in 04-07). - It defers writes (write-behind): it accumulates the SQL and sends it at the last moment, which allows it to be batched and ordered.
In a typical Spring application the persistence context lives as long as the transaction, usually a @Transactional method on a service. Outside it, entities are left detached. That link between context and transaction is the axis of this module and the reason lesson 04-07 exists.
- The four states of an entity
Every instance of an @Entity class is always in one of these four states. Knowing which one it is in explains why a change is saved or lost.
| State | Has an id? | Is it in the context? | Is it synchronised with the DB? |
|---|---|---|---|
| Transient (transient/new) | Normally not | No | No |
| Managed (managed/persistent) | Yes | Yes | Yes, automatically |
| Detached (detached) | Yes | No | No |
| Removed (removed) | Yes | Yes, marked for deletion | It will be deleted on flush |
stateDiagram-v2
[*] --> Transient: new Station()
Transient --> Managed: persist() / save()
Managed --> Detached: end of transaction / detach() / clear()
Detached --> Managed: merge()
Managed --> Removed: remove() / delete()
Removed --> Managed: persist()
Removed --> [*]: commit (DELETE)
Detached --> [*]: garbage collector
Let's walk through the four with a Ribalta station:
// 1. TRANSIENT: a plain, ordinary Java object.
Station station = new Station("Main Square", "Main Square 1", 24);
// Hibernate does not know it exists. If the JVM ends, it is lost.
// 2. MANAGED: from save() onwards, the context watches it.
Station managed = stationRepository.save(station);
managed.setCapacity(26);
// There is NO need to call save() again: dirty checking
// will detect the change and issue the UPDATE on commit.
// 3. DETACHED: when the transaction ends, the context closes.
// From here on, changes no longer propagate on their own:
managed.setCapacity(30); // this change does NOT reach the database
// 4. To reattach it you have to merge it:
Station reattached = stationRepository.save(managed); // performs a mergeTwo consequences that surprise everybody the first time:
- Step 2 does not need an explicit
save(). A great deal of Spring code calls it "just in case"; it is harmless but it reveals that the persistence context has not been understood. - Step 3 does need it, and there
save()does not insert: it merges. The difference is detailed in 04-05.
The detached state is the one that produces the dreaded LazyInitializationException: trying to navigate a lazy association on an entity whose context is already closed. We will see it in 04-04.
- What Spring Boot generates when it detects the starter
As soon as spring-boot-starter-data-jpa is on the classpath and there is a configured DataSource, the autoconfiguration from 02-06 fires. The classes involved are DataSourceAutoConfiguration, HibernateJpaAutoConfiguration and JpaRepositoriesAutoConfiguration, and between them they register these beans without you writing a single line:
| Bean created | What it does | Condition |
|---|---|---|
DataSource (HikariCP) |
Connection pool | There is a spring.datasource.url or an embedded DB |
EntityManagerFactory |
Factory for the EntityManager; builds the persistence unit |
There is a DataSource + Hibernate |
EntityManager (per-transaction proxy) |
The gateway to JPA | There is an EntityManagerFactory |
JpaTransactionManager |
Transaction manager for @Transactional |
There is an EntityManagerFactory |
JpaVendorAdapter |
Adapts Hibernate to Spring's abstraction | Hibernate on the classpath |
| Repositories | One proxy per interface extending Repository |
Implicit @EnableJpaRepositories |
PlatformTransactionManager |
The basis of transaction management (04-07) | — |
It also scans @Entity classes automatically from the package of the class annotated with @SpringBootApplication downwards. Since CicloUrbanaApplication lives in com.ciclourbana, every entity in com.ciclourbana.stations, .bikes and .rentals is detected on its own. It is the same component-scanning rule from 01-04, applied to entities.
You can verify it with the autoconfiguration report from 02-06:
Under Positive matches you will see entries such as:
HibernateJpaAutoConfiguration matched:
- @ConditionalOnClass found required classes 'jakarta.persistence.EntityManager',
'org.hibernate.SessionFactory' (OnClassCondition)
- @ConditionalOnBean found bean 'dataSource' (OnBeanCondition)
JpaRepositoriesAutoConfiguration#jpaRepositoriesFactoryBean matched:
- @ConditionalOnBean found bean 'entityManagerFactory'And if you do not configure a DataSource while having the starter, startup fails with a very characteristic message:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded
datasource could be configured.
Reason: Failed to determine a suitable driver classThat is exactly the problem the next lesson solves.
- Alternatives to JPA in the Spring ecosystem
JPA is the default option, not the only one and not always the best. Knowing the map avoids the mistake of forcing it where it does not fit.
| Technology | Level of abstraction | Strengths | Weaknesses | When to choose it |
|---|---|---|---|---|
| JdbcTemplate | Low: you write the SQL | Total control, no surprises, no state | Manual mapping, repetitive code | Reports, complex queries, bulk loads |
| Spring Data JDBC | Medium | Simple model, no cache or lazy loading, very predictable | No lazy relationships or inheritance | DDD models with well-bounded aggregates |
| Spring Data JPA | High | Productivity, relationships, cache, portability | Learning curve, implicit behaviour | CRUD over a rich domain, the general case |
| jOOQ | Low, with typed SQL | SQL checked at compile time, perfect for complex queries | Requires code generation; commercial licence on proprietary databases | Applications where SQL is the centre |
| MyBatis | Low-medium | SQL in XML or annotations, flexible mapping | More configuration, less automation | Migrating applications with legacy SQL |
| R2DBC | Medium | Reactive, non-blocking access | Smaller ecosystem, JPA not possible | WebFlux applications with heavy concurrency |
A practical and honest criterion: JPA shines when you write and read domain aggregates; it gets in the way when you produce reports. In CicloUrbana we will use Spring Data JPA for all the CRUD over stations, bikes and rentals, and in 04-06 we will see native queries for cases such as "nearest stations", where PostgreSQL-specific SQL wins hands down. They are not mutually exclusive: they coexist in the same project over the same DataSource.
- CicloUrbana's target data model
This is where the module is heading. Six tables that reflect the business of the Ribalta network:
erDiagram
STATIONS ||--o{ BIKES : "hosts"
STATIONS ||--o{ RENTALS : "origin"
STATIONS ||--o{ RENTALS : "destination"
BIKES ||--o{ RENTALS : "is rented in"
BIKES ||--o{ INCIDENTS : "accumulates"
USERS ||--o{ RENTALS : "makes"
FARES ||--o{ RENTALS : "is applied in"
STATIONS {
bigint id PK
varchar name UK
varchar address
int capacity
numeric latitude
numeric longitude
boolean active
bigint version
}
BIKES {
bigint id PK
varchar plate UK
varchar status
int battery_level
bigint station_id FK
bigint version
}
USERS {
bigint id PK
varchar email UK
varchar name
varchar fare_type
date signup_date
}
RENTALS {
bigint id PK
bigint user_id FK
bigint bike_id FK
bigint origin_station_id FK
bigint destination_station_id FK
timestamp started_at
timestamp ended_at
numeric total_amount
}
FARES {
bigint id PK
varchar code UK
numeric price_per_minute
numeric minimum_amount
}
INCIDENTS {
bigint id PK
bigint bike_id FK
varchar type
varchar description
timestamp reported_at
}
Notice three details that anticipate specific lessons:
- The
versioncolumn instationsandbikesis the optimistic locking of 04-03, the one that will replace theShallowEtagHeaderFilterfrom 03-03. rentalshas two foreign keys tostations(origin and destination): a case wheremappedByis not enough and each@JoinColumnhas to be named explicitly (04-04).total_amountandprice_per_minutearenumeric, neverdouble. The reason is in 04-03, and it is one of the least negotiable rules in the whole module.
The route through the module, resting on this model:
| Lesson | What it adds to CicloUrbana |
|---|---|
| 04-02 | H2 in development, PostgreSQL 16 in Docker, HikariCP tuned |
| 04-03 | Station, Bike and Rental as entities with @Version and auditing |
| 04-04 | The relationships in the diagram, LAZY and the N+1 problem |
| 04-05 | StationRepository extends JpaRepository, pagination in the API |
| 04-06 | Derived queries, JPQL, native queries and projections |
| 04-07 | @Transactional in the services, propagation and locks |
| 04-08 | Flyway: the schema as versioned code |
Common Mistakes and Tips
Believing that JPA is Hibernate. They are different things: JPA is the specification, Hibernate the implementation. It matters when an annotation such as @BatchSize shows up, which belongs to Hibernate, not to JPA: it works, but it ties you to the provider. Always check the package in the import: jakarta.persistence.* is standard; org.hibernate.annotations.* is provider-specific.
Thinking an ORM spares you from knowing SQL. It is exactly the other way round: the more automatic the SQL generation, the more important it is to be able to read it. Turn on statement logging from day one (04-02) and get into the habit of looking at it.
Calling save() on an already managed entity. It does no harm, but it signals that dirty checking has not been understood. Inside a @Transactional method, modifying a loaded entity is enough.
Forgetting that the persistence context dies with the transaction. It is the origin of LazyInitializationException and of "changes that don't get saved". When something odd happens, the first question must be: what state is this entity in?
Choosing JPA for everything, reports included. An aggregation query over a million rentals should not go through entity mapping. JdbcTemplate or a native query with a projection is the right answer (04-06).
Tip: draw the relational model before annotating classes. The ER diagram above was drawn before the entities, not after. Annotating without a considered model produces schemas that have to be redone, and in 04-08 you will see that redoing a schema in production is expensive.
Tip: keep the StationRepository interface. The whole module is the demonstration that isolating storage behind an interface lets you change technology without touching services or controllers.
Exercises
Exercise 1: classify the state of the entities
Given the following method from a CicloUrbana service, state which state the station variable is in at each marked point and whether the change on the final line reaches the database. Justify your answer.
@Transactional
public void renameStation(Long id, String newName) {
Station station = new Station(); // (A)
station = stationRepository.findById(id) // (B)
.orElseThrow(() -> new ResourceNotFoundException("Station", id));
station.setName(newName); // (C)
} // (D) end of methodExercise 2: choose the data access technology
For each of the Ribalta council's needs, choose between JdbcTemplate, Spring Data JPA, jOOQ or R2DBC, and justify your choice in one sentence.
- Full CRUD over stations with validation and relationships with bikes.
- A monthly report that crosses rentals, users and fares with six
JOINs, three subqueries and window functions. - Loading 500,000 sensor readings from the bikes in one go every night.
- A real-time dashboard holding 20,000 open connections showing station occupancy.
Exercise 3: walk through the layers
Explain, layer by layer, what happens when StationService runs stationRepository.findById(3L) and the "River Park" station is not yet in the persistence context. Name the five layers involved and what each one does.
Solutions
Solution 1.
- (A) Transient.
new Station()creates an ordinary Java object; Hibernate does not know it, it has no id and nothing watches it. Besides, this line is useless: the reference is overwritten immediately. It is a code smell, not an error. - (B) Managed.
findByIdloads the row and attaches it to the persistence context of the transaction opened by@Transactional. Hibernate keeps a copy of its original state for dirty checking. - (C) Managed and "dirty". The object is still managed; its name now differs from the original copy. No
UPDATEhas been executed yet. - (D) The change DOES reach the database. When the method ends, the transactional proxy commits; before the commit a flush runs that compares the current state with the original copy, detects the modified name and issues
UPDATE stations SET name = ? WHERE id = ?. After the commit the context closes and the entity becomes detached.
There is no need to call save(). This is the canonical example of dirty checking. If the method did not carry @Transactional, the context would live only for the duration of the repository call, the entity would come back detached and the change would be lost silently: the worst kind of failure, because it throws no exception at all.
Solution 2.
- Spring Data JPA. This is its ideal case: a domain aggregate, relationships, validation, writing and reading by identifier. The productivity more than compensates for the abstraction.
- jOOQ (or
JdbcTemplatewith native SQL). SixJOINs, subqueries and window functions are not comfortably expressible in JPQL, and forcing it produces unreadable queries. jOOQ gives typed SQL checked at compile time;JdbcTemplateis the alternative with no added dependencies. JdbcTemplatewithbatchUpdate. A bulk load must not go through the persistence context: 500,000 managed entities would exhaust memory and dirty checking would do pointless work. Batch insertion in plain JDBC.- R2DBC with WebFlux. Twenty thousand concurrent connections with JDBC's blocking model would demand twenty thousand threads. Non-blocking reactive access is designed for exactly this. (With Java 21 virtual threads change part of this calculation, but R2DBC is still the by-the-book option.)
Solution 3.
StationServicecallsstationRepository.findById(3L)on the interface. It knows nothing about JPA.- The Spring Data JPA proxy —created at startup, as we will see in 04-05— receives the call and delegates it to
SimpleJpaRepository, the standard implementation. SimpleJpaRepositorytranslates it intoentityManager.find(Station.class, 3L)and wraps the result in anOptional.- The
EntityManager(Hibernate) looks in the persistence context first. As it is not there, it generates SQL adapted to the dialect:select s1_0.id, s1_0.capacity, ... from stations s1_0 where s1_0.id=?. - JDBC and the PostgreSQL driver request a connection from the HikariCP pool, send the prepared statement and return a
ResultSet.
On the way back, Hibernate builds the Station instance, registers it in the persistence context with a copy of its state, and returns it wrapped in an Optional. If the service asked for station 3 again in the same transaction, step 5 would not be repeated: the first-level cache would return the same object.
Conclusion
You now have the full map of the terrain. You know that the object-relational impedance mismatch is not a whim but five concrete disagreements between two equally valid models, and that an ORM exists to absorb them in exchange for introducing rules of its own. You can tell apart with precision the four pieces that get confused daily: JDBC as the low-level API, JPA as the specification, Hibernate as the implementation that generates the real SQL, and Spring Data JPA as the layer that manufactures repositories from interfaces. You know the seven dependencies that spring-boot-starter-data-jpa pulls in and, above all, the one it does not: the driver. You understand what the persistence context is and its three properties —unique identity, dirty checking and deferred writing—, which explain why modifying a managed entity is enough to save it. You can place any instance in one of the four states and predict whether its changes will survive. You know which beans the autoconfiguration creates as soon as it detects the starter and how to verify it with --debug. And you have the judgement to know when JPA is not the answer and JdbcTemplate, jOOQ or R2DBC is called for.
You also have the destination in sight: the six tables of the CicloUrbana model, with their version column for optimistic locking, the two foreign keys from rentals to stations and their amounts in numeric.
One thing is still missing, the very first one: there is no database at all. If you add the starter right now and start up, the application will fail with "Failed to configure a DataSource". Lesson 04-02, Configuring Data Sources, solves that: we will set up in-memory H2 for development with its web console, bring up PostgreSQL 16 with a docker-compose.yml of CicloUrbana's own, tune the HikariCP pool parameter by parameter with real sizing criteria, decide which ddl-auto value to use in each environment and why open-in-view must be switched off from day one, and leave SQL logging configured so we can see exactly what Hibernate sends to Ribalta.
Spring Boot Course
Module 1: Introduction to Spring Boot
- What Is Spring Boot?
- Setting Up Your Development Environment
- Building Your First Spring Boot Application
- Understanding the Project Structure
- Application Startup and Lifecycle
Module 2: Spring Boot Core Concepts
- Spring Boot Annotations
- Dependency Injection in Spring Boot
- Bean Scope and Lifecycle
- Spring Boot Configuration
- Spring Boot Properties
- Auto-Configuration and Starters from the Inside
Module 3: Building RESTful Web Services
- Introduction to RESTful Web Services
- Creating REST Controllers
- Handling HTTP Methods
- Validating Input Data
- DTOs and Mapping Between Layers
- Exception Handling in REST
- Documenting the API with OpenAPI
Module 4: Data Access with Spring Boot
- Introduction to Spring Data JPA
- Configuring Data Sources
- Creating JPA Entities
- Relationships Between Entities
- Using Spring Data Repositories
- Query Methods in Spring Data JPA
- Transactions and Persistence Management
- Schema Migrations with Flyway
Module 5: Security in Spring Boot
- Introduction to Spring Security
- Configuring Spring Security
- User Authentication and Authorization
- Implementing JWT Authentication
- Method-Level Security and API Hardening
Module 6: Testing in Spring Boot
- Introduction to Testing
- Unit Testing with JUnit
- Mocking with Mockito
- Integration Testing
- Testing with Testcontainers
Module 7: Advanced Spring Boot Features
- Spring Boot Actuator
- Spring Boot Profiles
- Scheduled Tasks and Asynchronous Execution
- Spring Boot with Docker
- Spring Boot and Microservices
- Service Communication and Fault Tolerance
Module 8: Deploying Spring Boot Applications
- Introduction to Deployment
- Deploying to Heroku
- Deploying to AWS
- Deploying to Kubernetes
- Continuous Integration and Delivery
Module 9: Performance and Monitoring
- Performance Tuning
- Caching with Spring Cache
- Monitoring with Spring Boot Actuator
- Using Prometheus and Grafana
- Logging and Log Management
- Distributed Tracing
