The previous module ended with a promise: "You have written miniature versions of almost all of them with your own hands. Now you are going to use the real ones."
This lesson is the bridge. Before writing your first Spring annotation or your first Hibernate entity, you need to understand what exactly a framework is, how it differs from a library, what it gives you and what it takes away, and —above all— how it works on the inside, because you already know the answer to that last question: annotations, reflection, dynamic proxies and code generation. Exactly what you built in module 10.
This matters for a very practical reason. The difference between a developer who uses Spring and one who understands Spring is not how many annotations they know by heart: it is that when something breaks —and it will— the first one searches a forum and the second one reasons about what the container is doing. When you see that a call to a @Transactional method from another method of the same class opens no transaction, you will know why: because the dynamic proxy you wrote in 10-03 has exactly that same hole, and you saw it with your own eyes.
There is also a professional decision that nobody teaches you and that you will take dozens of times in your career: whether or not to add a dependency. It is a decision with security, maintenance and weight consequences. This lesson gives you real criteria for taking it.
By the end you will be able to tell a library from a framework and explain inversion of control with an example you wrote yourself, you will have a map of the Java ecosystem by category, you will know how a dependency is evaluated before it goes into your pom.xml, you will understand what Maven Central is and what the groupId:artifactId:version coordinates mean, and you will know about software supply chain risk through a real case that paralysed the industry for weeks.
Contents
- The starting point: BiblioTech, hand-made
- Library versus framework: who calls whom
- Inversion of control, explained with your own
SimpleContainer - What a framework gives you
- What a framework costs you
- How the magic holds up: the four mechanisms
- The map: what you wrote by hand versus what the real framework does
- A map of the Java ecosystem by category
- How a dependency is chosen
- The prior question: do I really need it?
- Maven Central and the coordinates
- Semantic versioning
- The cost of supply chain security
- Log4Shell: what actually happened
- Auditing dependencies in practice
- Which framework to learn first
- Why this course chooses Spring Boot
- The plan for module 11
- Common Mistakes and Tips
- Exercises
- The starting point: BiblioTech, hand-made
Before looking ahead, take one more look at what you have. BiblioTech, at the end of module 10, is a respectable project: a sealed domain, generic repositories, streams, java.time, virtual threads, a home-made dependency injection container and three dynamic proxies.
And it has six declared debts:
| Debt | Current state | What is missing |
|---|---|---|
| Dependency injection | SimpleContainer, 150 lines |
Scopes, lifecycle, per-environment configuration, transactions |
| Persistence | CSV files | Queries, indexes, transactions, referential integrity |
| JSON | Hand-written indexOf (09-06) |
A real parser |
| Boilerplate code | Hand-written getters, equals, hashCode, toString |
Automatic generation |
| Logging | java.util.logging |
A standard facade from the ecosystem |
| Building | Hand-written javac |
Dependencies, phases, packaging, reproducibility |
| Testing | None | Everything |
Every one of those rows is a lesson in this module. But notice something: none of those debts is business logic. None of them has anything to do with loans, fines, reservations or catalogues. They are all plumbing: things every application needs and no application wants to write.
That observation is, literally, the definition of the problem frameworks solve.
- Library versus framework: who calls whom
The distinction is usually explained badly. It is not a question of size or complexity. There are enormous libraries and tiny frameworks. The difference is the direction of the calls.
- A library is code that you call. You control the flow: you decide when, where and with which arguments.
java.util.Collections, Jackson or Apache Commons are libraries: you callCollections.sort(list)whenever it suits you. - A framework is code that calls you. The framework controls the flow: it defines the structure of the application, it starts up, and at certain points it invokes your code. You fill in the gaps.
This inversion has a classic name, the Hollywood Principle: "Don't call us, we'll call you."
graph LR
subgraph "LIBRARY"
A1["Your code<br/>(controls the flow)"] -->|calls| B1["Jackson<br/>Commons<br/>Guava"]
B1 -->|returns| A1
end
subgraph "FRAMEWORK"
A2["Spring<br/>JUnit<br/>Servlet API"] -->|calls| B2["Your code<br/>(fills the gaps)"]
B2 -->|returns| A2
end
A concrete example you already know, from 09-06. When you wrote MetadataClient with HttpClient:
// LIBRARY: you decide when to call
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
String body = response.body();You control everything. HttpClient is a library.
And now, a framework example you will see in 11-04:
Where is the main? Who calls fineForFiveDaysIsFiveEuros()? Not you. JUnit calls it. All you did was write a method and put a label on it. JUnit scans your classes, finds the methods annotated with @Test, creates one instance per method and invokes them. That is a framework.
Compare the two models:
| Aspect | Library | Framework |
|---|---|---|
| Control of the flow | Yours | The framework's |
| Who calls whom | You → library | Framework → you |
| Structure of your code | Free | Imposed (conventions, annotations, interfaces) |
| Cost of replacing it | Low (you isolate the call) | High (it permeates the architecture) |
| Examples | Jackson, Guava, Commons, SLF4J | Spring, JUnit, Hibernate, Quarkus |
| Entry point | Methods you invoke | Hooks: annotations, interfaces, configuration files |
An honest nuance: the boundary is not always sharp. Spring is a framework, but RestTemplate inside Spring is a library. Hibernate is a framework when it manages the lifecycle of your entities, and a library when you call entityManager.find(...). The useful thing is not to classify, but to ask at each point: who controls the flow here?
- Inversion of control, explained with your own
SimpleContainer
SimpleContainerInversion of control (IoC) is the general principle: handing control of the flow over to another component. Dependency injection (DI) is the concrete form of IoC you will use most: instead of an object creating its own dependencies, they are handed to it ready-made.
You wrote this yourself in 10-03. Before SimpleContainer, LoanManager did this:
// BEFORE: the object creates its own dependencies. Control NOT inverted.
public class LoanManager {
private final Repository<Loan> repository;
private final FineCalculator calculator;
private final NoticeService notices;
public LoanManager() {
// The manager itself decides WHICH implementation to use and HOW to build it.
this.repository = new LoanStore(Path.of("data/loans.csv"));
this.calculator = new FineCalculator(Clock.systemDefaultZone());
this.notices = new NoticeService("smtp.nexussoftware.com", 587);
}
}The problems with that code, all of them real:
- It cannot be tested. To test a fine calculation you need a CSV file on disk and an SMTP server.
- You cannot change implementation. If tomorrow the store becomes a database, you have to edit
LoanManager. - It cannot be configured per environment. The development SMTP host and the production one are different, and they are hard-coded.
- The clock is the system's. That
Clock.systemDefaultZone()makes it impossible to test a due date without waiting 15 days. Precisely what 10-05 set out to avoid.
After SimpleContainer:
// AFTER: the object DECLARES what it needs. Control inverted.
public class LoanManager {
private final Repository<Loan> repository;
private final FineCalculator calculator;
private final NoticeService notices;
public LoanManager(Repository<Loan> repository,
FineCalculator calculator,
NoticeService notices) {
this.repository = repository;
this.calculator = calculator;
this.notices = notices;
}
}LoanManager no longer knows where its collaborators come from. It only knows what it needs. Somebody on the outside —the container— resolves the dependency graph, builds everything in the right order and hands it the pieces.
Visually, the change is this:
graph TD
subgraph "WITHOUT inversion of control"
G1["LoanManager"] -->|new| R1["LoanStore"]
G1 -->|new| C1["FineCalculator"]
G1 -->|new| A1["NoticeService"]
end
subgraph "WITH inversion of control"
CT["IoC container"] -->|builds| R2["LoanStore"]
CT -->|builds| C2["FineCalculator"]
CT -->|builds| A2["NoticeService"]
CT -->|builds and injects| G2["LoanManager"]
R2 -.->|injected| G2
C2 -.->|injected| G2
A2 -.->|injected| G2
end
Your SimpleContainer did this: it scanned the classes annotated with @Component, looked at their constructors through reflection, recursively resolved each parameter, detected cycles and cached the singletons. One hundred and fifty lines.
Spring's ApplicationContext does exactly the same. And a few things more:
| Capability | Your SimpleContainer |
Spring's ApplicationContext |
|---|---|---|
| Component scanning | Yes, @Component |
Yes, @Component and stereotypes |
| Constructor injection | Yes | Yes (recommended) |
| Setter and field injection | No | Yes |
| Cycle detection | Yes, with an exception | Yes, with a far better message |
| Singleton | Yes, one Map |
Yes, plus 5 additional scopes |
| Prototype / request / session scope | No | Yes |
Lifecycle (@PostConstruct / @PreDestroy) |
No | Yes |
| Per-environment configuration (profiles) | No | Yes, @Profile |
| Externalised properties | No | Yes, @Value, @ConfigurationProperties |
| Ambiguity resolution | No (it failed with 2 implementations) | Yes, @Qualifier, @Primary |
Injecting every implementation into a List<T> |
No | Yes |
| Declarative transactions | No | Yes, @Transactional |
| Aspect-oriented programming | Three hand-written proxies | Yes, built in |
| Lazy creation | No | Yes, @Lazy |
| Events between components | No | Yes, ApplicationEventPublisher |
| Web application start-up | No | Yes |
None of those extra rows is business logic. They are all things you would need to write if your application grew. That is what you are buying.
- What a framework gives you
Four concrete things, in order of real importance.
1. Non-differentiating code, already solved. There is a useful distinction between differentiating code (the code that solves the problem your company exists for: in BiblioTech, the rules for loans, fines and reservations) and plumbing code (what any application needs: starting up, configuring itself, talking to a database, serving HTTP, recording traces). Plumbing code adds value for nobody and yet it eats most of your time when you write it yourself. A framework gives you that time back.
2. Shared conventions. If you hire a Java developer with Spring experience, they understand your project structure in an afternoon. If BiblioTech used your home-made SimpleContainer, it would take them a week to understand a container that exists only in your company and for which there is no documentation, no courses and no forum answers. Conventions are an economic asset.
3. Ecosystem. When you choose Spring, you are not just choosing a container: you are choosing Spring Data (persistence), Spring Security (authentication), Spring Cloud (distributed systems), Actuator (observability) and hundreds of integrations that already work together. The integration is already done and tested, and that is worth more than any individual feature.
4. Maintained security. When a vulnerability appears in HTTP request handling, the Spring team publishes a patch and you bump a version. If that logic is yours, you discover the vulnerability when somebody exploits it. This point tends to be underrated and it is probably the most important one in production.
- What a framework costs you
A framework is not free. Four costs, also real.
1. Learning curve. Spring has more than twenty years of accumulated surface. Becoming productive takes weeks; becoming competent, months. And a good part of that learning does not transfer: learning @Transactional teaches you nothing about databases, it teaches you about Spring.
2. Coupling. A framework permeates the architecture. Migrating an application from Spring to Quarkus is not swapping a dependency: it is rewriting the configuration layer, the start-up layer, the test layer and probably part of the domain. That is why it is worth keeping the domain free of framework annotations wherever possible — an idea developed in 12-02.
3. Magic that is hard to debug. When an @Autowired does not inject, or a transaction does not roll back, or an entity saves itself without you calling save, the cause is in code you did not write, invoked by reflection from a point that does not appear in your stack trace. This is exactly why module 10 came before this one: if you know how the magic works, you can debug it.
4. Weight. An executable Spring Boot jar with web and JPA weighs around 40-50 MB and starts in 1-3 seconds. For a long-lived service that is irrelevant. For a serverless function that starts on every request or a CLI that must respond in 50 ms, it is disqualifying — and that is where Quarkus, Micronaut or the GraalVM native compilation mentioned in 10-07 come in.
In summary:
| You gain | You pay |
|---|---|
| Weeks of plumbing already solved | Weeks of learning curve |
| Conventions anybody understands | Coupling to a specific architecture |
| An integrated, tested ecosystem | Implicit behaviour that is hard to debug |
| Maintained security patches | Third-party attack surface |
| Less code of your own to maintain | More weight, more start-up, more memory |
The professional conclusion is neither "frameworks are good" nor "they are bad": it is that the decision depends on the context, and that anyone who does not understand what lies underneath cannot take it.
- How the magic holds up: the four mechanisms
This is where module 10 pays off in full. Everything a Java framework does "by magic" rests on four mechanisms, and you know all four.
Mechanism 1: annotations (10-02)
Annotations are metadata: labels that do nothing on their own. @Service starts nothing. @Entity creates no table. @Test runs nothing. They are inert marks that somebody has to read.
You learned that by building @CsvField and @Auditable. And you learned the indispensable condition:
@Retention(RetentionPolicy.RUNTIME) // without this, the annotation does NOT exist at runtime
@Target(ElementType.TYPE)
public @interface Component { }Every framework annotation that is read at runtime carries @Retention(RUNTIME). Open the source of Spring's @Service or Jakarta Persistence's @Entity: it is there.
Mechanism 2: reflection (10-03)
Somebody has to read the labels. That somebody uses reflection: Class.forName, getDeclaredFields, isAnnotationPresent, getDeclaredConstructor().newInstance(), invoke, setAccessible(true).
Your AnnotatedExporter walked the fields of any object looking for @CsvField. Hibernate walks the fields of any entity looking for @Column. It is the same loop.
Mechanism 3: dynamic proxies (10-03)
When the framework needs to add behaviour around your method without touching your code —transactions, security, caching, retries, metrics— it does not modify your class: it wraps it. You wrote three:
// Your AuditProxy from 10-03
Object proxy = Proxy.newProxyInstance(
iface.getClassLoader(),
new Class<?>[] { iface },
(p, method, args) -> {
if (method.isAnnotationPresent(Auditable.class)) {
auditLog.recordStart(method.getName());
}
Object result = method.invoke(target, args); // the real call
// ... post-processing
return result;
});When you see @Transactional in 11-02, you will see nothing new: you will see that same InvocationHandler with begin transaction before and commit or roll back after. And you will see the same hole too: if the target calls itself internally, the call does not go through the proxy and the aspect is not applied. That detail is the cause of a huge percentage of the world's "my @Transactional doesn't work" reports.
Technical note: the JDK proxies you wrote only work over interfaces. When the class implements none, Spring uses CGLIB, which generates a subclass of your class at runtime and overrides its methods. Hence a practical consequence: a final class or method cannot be proxied with CGLIB, and that is why sometimes your @Transactional on a final method does absolutely nothing.
Mechanism 4: code generation
There are two possible moments.
- At compile time, through an annotation processor (
javax.annotation.processing, seen in 10-02). That is what Lombok does: there is no runtime magic, the.classalready contains the getters. Also MapStruct and Micronaut. - At runtime, generating bytecode on the fly with ASM, ByteBuddy or CGLIB. That is what Spring, Hibernate and Mockito do.
Each option has its consequences:
| Mechanism | When it acts | Start-up cost | Debuggable | Examples |
|---|---|---|---|---|
| Annotations + reflection | Runtime | Medium (scanning) | So-so | Spring, Hibernate, JUnit, Jackson |
| JDK dynamic proxy | Runtime | Low | So-so (traces with $Proxy0) |
@Transactional, Spring Data |
| Subclass generation (CGLIB/ByteBuddy) | Runtime | Medium | Hard | Spring AOP over classes, Mockito |
| Annotation processor | Compile time | None | Easy (the code exists) | Lombok, MapStruct, Micronaut |
The current trend, precisely because of start-up cost and compatibility with GraalVM native compilation, is to move work from runtime to compile time. Quarkus and Micronaut were born with that idea, and Spring 6 has partly adopted it with AOT.
- The map: what you wrote by hand versus what the real framework does
This table is module 10 summarised and read from module 11. It is probably the most important table in this lesson.
| What you wrote by hand | Lesson | What the real framework does | Seen in |
|---|---|---|---|
SimpleContainer with @Component and @Inject |
10-03 | Spring's ApplicationContext with @Component and @Autowired |
11-02 |
AuditProxy, RetryProxy, TimingProxy |
10-03 | Spring AOP, @Transactional, @Retryable, @Cacheable |
11-02 |
AnnotatedExporter reading @CsvField by reflection |
10-03 | Hibernate reading @Column, Jackson reading @JsonProperty |
11-03, 11-07 |
AnnotatedValidator with @Validate |
10-03 | Jakarta Bean Validation (@NotNull, @Size, @Email) |
11-02 |
Hand-written CsvReader / CsvWriter |
07-07 | OpenCSV, Commons CSV — and, better still, a database | 11-03, 11-07 |
LoanStore over files |
07-06 | Hibernate / JPA over a relational database | 11-03 |
Generic Repository<T extends Identifiable> |
10-01 | Spring Data's JpaRepository<T, ID>, implemented with proxies |
11-03 |
JSON parsing with indexOf |
09-06 | Jackson's ObjectMapper |
11-07 |
Hand-written getters, equals, hashCode, toString |
03-09 | Lombok (or record, which you already use) |
11-07 |
LogConfiguration with java.util.logging |
06-07 | SLF4J + Logback | 11-07 |
Configuration / BusinessRules with Properties |
07-07 | application.yml, @ConfigurationProperties, profiles |
11-02 |
A main that prints and a human who looks |
all | JUnit 5 + AssertJ + Mockito | 11-04, 11-06 |
javac with a hand-written classpath |
01-02 | Maven | 11-05 |
GracefulShutdown with shutdown hooks |
08-05 | The container lifecycle, @PreDestroy |
11-02 |
Read it in both directions. Left to right, it is your road map. Right to left, it is the answer to the question "what is Spring doing here?".
- A map of the Java ecosystem by category
The Java ecosystem is enormous and a newcomer finds it overwhelming. In reality it sorts nicely into eight categories, and in each one there are two or three dominant options.
Building and dependencies
| Tool | What it is | When | Seen in |
|---|---|---|---|
| Maven | The de facto standard. Declarative XML, fixed lifecycle | Default in the enterprise | 11-05 |
| Gradle | A Groovy or Kotlin DSL, caching and incremental builds | Large projects, Android | 11-05 (compared) |
| Ant | Historical, imperative | Legacy only | — |
Dependency injection and applications
| Framework | What it is | Strong at | Seen in |
|---|---|---|---|
| Spring / Spring Boot | The dominant ecosystem | Everything; enormous community | 11-02 |
| Quarkus | Cloud-native, start-up in milliseconds | Containers, serverless | mentioned |
| Micronaut | DI resolved at compile time | Minimal start-up and memory | mentioned |
| Jakarta EE | The standard (CDI, JAX-RS, JPA) | Application servers | 11-03 (JPA) |
Persistence
| Library | Approach | When | Seen in |
|---|---|---|---|
| JPA / Hibernate | ORM: objects ↔ tables | Rich domain, CRUD | 11-03 |
| Spring Data JPA | Convention-based repositories over JPA | With Spring | 11-03 |
| jOOQ | SQL with types checked at compile time | Complex SQL and full control | mentioned |
| MyBatis | SQL in XML or annotations, explicit mapping | Bespoke queries | mentioned |
| JDBC / JdbcTemplate | Direct, no ORM | One-off queries, performance | 11-03 (baseline) |
Testing
| Library | What for | Seen in |
|---|---|---|
| JUnit 5 | The testing framework | 11-04 |
| Mockito 5 | Test doubles | 11-06 |
| AssertJ | Fluent, readable assertions | 11-04, 11-06 |
| Testcontainers | Real databases and services in Docker | 11-06, 12-05 |
| WireMock | Simulating HTTP APIs | mentioned |
| JaCoCo | Coverage | 12-05 |
Serialisation
| Library | Notes | Seen in |
|---|---|---|
| Jackson 2.x | The standard; built into Spring | 11-07 |
| Gson | Google's; simpler, less powerful | 11-07 (mentioned) |
| JSON-B | The Jakarta standard | 11-07 (mentioned) |
Logging
| Piece | Role | Seen in |
|---|---|---|
| SLF4J | Facade: the API you program against | 11-07 |
| Logback | Spring Boot's default implementation | 11-07 |
| Log4j2 | An alternative implementation, very fast | 11-07 (and §14) |
java.util.logging |
The JDK's own; no dependencies, limited | 06-07 |
Utilities
| Library | What it brings | Seen in |
|---|---|---|
| Lombok | Generates boilerplate at compile time | 11-07 |
| Guava | Collections, caching, utilities (Google) | 11-07 (overview) |
| Apache Commons Lang / IO | String, file and reflection utilities |
11-07 (overview) |
| MapStruct | Entity ↔ DTO mappers at compile time | 11-07 (overview) |
| Caffeine | High-performance in-memory cache | 11-07 (overview) |
Documentation and operations
| Library | What it brings | Seen in |
|---|---|---|
| springdoc-openapi | Automatic OpenAPI/Swagger documentation | 11-07, 12-04 |
| Micrometer | Metrics | 11-07, 12-07 |
| Resilience4j | Retries, circuit breakers, rate limiters | 11-07, 12-07 |
| Flyway / Liquibase | Versioned schema migrations | 11-03, 12-06 |
With this map in front of you, the ecosystem stops being an endless list of names and becomes eight decisions, each with a reasonable default.
- How a dependency is chosen
Adding a line to the pom.xml takes five seconds and commits your team for years. These are the criteria that are actually used.
| Criterion | What to look at | Warning sign |
|---|---|---|
| Active maintenance | Date of the latest release, open and closed issues, release cadence | No releases for >2 years; issues with no answer |
| Licence | Apache 2.0, MIT and BSD are safe. GPL/AGPL are viral. Always check | GPL in a closed commercial product; missing licence |
| Adoption | Number of usages in Maven Central, stars, answered forum questions | Ten users worldwide; nobody has ever had your problem |
| Size and transitivity | How many jars does it drag in? mvn dependency:tree |
A 20 KB utility that drags in 30 MB |
| A JDK alternative | Do java.util, java.time or java.net.http already do it? |
Yes, and you add the dependency anyway |
| Compatibility | Minimum Java version, jakarta.* versus javax.* |
Requires Java 8 and has not migrated to jakarta |
| Security | Any open CVEs? Does it publish advisories? How fast does it patch? | Known, unfixed vulnerabilities |
| Documentation | Is there a getting-started guide, javadoc, examples? | Just a three-line README |
| Exit cost | If it disappears tomorrow, how much does replacing it cost? | It permeates the whole codebase |
A practical checklist before you write the dependency:
[ ] Do I really need it? Doesn't the JDK, or something I already have, do it?
[ ] Is the licence compatible with my product?
[ ] Has it released a version in the last 12 months?
[ ] Does it have open, unfixed CVEs?
[ ] How many transitive dependencies does it drag in?
[ ] Is it compatible with my Java version and with jakarta.*?
[ ] Is there a smaller or more standard alternative?
[ ] Do I know how I would remove it if I had to?If you hesitate on more than two boxes, do not add it yet.
- The prior question: do I really need it?
Before all the criteria above there is a question far too many people skip.
The modern JDK is much richer than people remember. A great many historical dependencies are no longer needed:
| It used to be added | Today it is already in the JDK | Since |
|---|---|---|
| Joda-Time | java.time |
Java 8 |
| Apache HttpClient (for the basics) | java.net.http.HttpClient |
Java 11 |
| Commons IO (for the basics) | java.nio.file.Files |
Java 7 |
Guava's Lists.newArrayList |
List.of, new ArrayList<>() |
Java 9 |
Commons Lang's StringUtils.isBlank |
String.isBlank() |
Java 11 |
| A library to read a whole file | Files.readString(path) |
Java 11 |
A record-like generated by Lombok |
record |
Java 16 |
| Commons Codec's Base64 | java.util.Base64 |
Java 8 |
And the counterweight: nor should you write yourself what is notoriously hard to get right. Cryptography, date parsing with time zones, JSON parsing, CSV parsing with embedded quotes and line breaks (remember your CsvReader from 07-07 and all the edge cases it did not cover), and anything to do with security. There, a mature dependency always beats your own implementation.
The practical rule: write yourself what is specific to your business; delegate what is generic and hard.
- Maven Central and the coordinates
When you add a dependency, where does the jar come from?
Maven Central is the public repository of Java artefacts, maintained by Sonatype. It holds millions of versions of hundreds of thousands of libraries, and it is Maven's and Gradle's default target. When you write a dependency and run mvn package, Maven downloads it from Central and stores it in your local repository (~/.m2/repository) so it never has to download it again.
Every artefact is identified by three coordinates:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.1</version>
</dependency>| Coordinate | What it is | Convention |
|---|---|---|
groupId |
The organisation or project | Reversed domain: org.springframework.boot |
artifactId |
The specific module | Short name: spring-boot-starter-web |
version |
The published version | Semantic versioning: 3.3.2 |
They are written in short form as groupId:artifactId:version, for example com.fasterxml.jackson.core:jackson-databind:2.17.1. You will see that notation in Maven's output, in security reports and throughout the documentation.
And the physical path is predictable: the groupId with its dots turned into folders, then the artifactId, then the version. The jar above lives at:
Knowing this genuinely helps the day Maven tells you it cannot find an artefact: you can go to that folder and see whether it is there, whether it is corrupt, or whether all you have is a .lastUpdated file (the classic symptom of a failed download, fixed by deleting the folder and running mvn -U).
All of this is developed in 11-05. Here you only need the vocabulary.
- Semantic versioning
Most of the Java ecosystem follows semantic versioning (SemVer), with the format MAJOR.MINOR.PATCH:
| Part | Incremented when | Compatibility | Example |
|---|---|---|---|
| MAJOR | There are breaking changes | Breaks | 2.7.18 → 3.0.0 |
| MINOR | Compatible functionality is added | Compatible | 3.2.0 → 3.3.0 |
| PATCH | Bugs are fixed | Compatible | 3.3.1 → 3.3.2 |
The usual suffixes, from least to most stable:
| Suffix | Meaning |
|---|---|
-SNAPSHOT |
In development, changes without warning. Never in production |
-M1, -M2 |
Milestone, very preliminary |
-RC1 |
Release candidate, almost final |
| (no suffix) | General availability release, immutable forever |
Two practical consequences:
- Bumping PATCH or MINOR should be safe, and it is what you do continuously to pick up security fixes.
- Bumping MAJOR requires reading the migration notes. The canonical and very current example: Spring Boot 2.x → 3.x replaced every
javax.*package withjakarta.*and requires Java 17. That is why everything about persistence in this course isjakarta.persistenceand notjavax.persistence. If you copy an example from the internet withjavax.persistence, it is from Spring Boot 2 and it will not compile.
- The cost of supply chain security
Here comes the uncomfortable part.
When you add a dependency, you do not add a jar: you add a tree. A single spring-boot-starter-web brings in around thirty transitive artefacts. A typical enterprise project has between 100 and 400 jars, of which you wrote zero.
Every one of those jars runs code with the same permissions as your application. It can read your files, open network connections and access your environment variables. And you have reviewed none of them.
This is called software supply chain risk, and it takes three forms:
| Risk | What it consists of | Example |
|---|---|---|
| Known vulnerability | A published flaw (CVE) in a version you use | Log4Shell, Spring4Shell |
| Abandoned dependency | Nobody patches it; the vulnerability will never be fixed | Libraries with no release since 2018 |
| Compromised artefact | Somebody publishes malicious code in a legitimate library | Documented cases in npm and PyPI |
- Log4Shell: what actually happened
In December 2021 CVE-2021-44228, nicknamed Log4Shell, was published against Apache Log4j 2, one of the most widely used logging libraries in the Java world.
The technical summary, without the drama: Log4j 2 supported variable substitution inside log messages, and one of the supported forms allowed a JNDI lookup against a remote server. Consequence: if an application logged text controlled by an attacker —an HTTP header, a user name, a User-Agent— the attacker could make the server download and run their code. Remote code execution from a single line of text.
Why it was so serious:
- Trivial to exploit: all it took was sending a string in any field that ended up in a log.
- Ubiquitous: Log4j 2 was in tens of thousands of products, very often as a transitive dependency the team did not even know it had.
- Hard to inventory: the question "do we have Log4j?" turned out not to be easy to answer in most organisations. And that was the real lesson.
What the industry learned, and what affects you directly:
- You have to know what you have. Hence the rise of the SBOM (Software Bill of Materials), the inventory of an application's components.
- You have to be able to update fast. If updating a dependency takes three weeks of manual testing because there are no automated tests, your exposure window is three weeks. This connects directly with the whole of module 11: without Maven you cannot change a version with one line, and without JUnit you cannot verify that the change broke nothing.
- You have to watch continuously. A project that is secure today has vulnerabilities six months from now without a single line of code changing, because vulnerabilities are discovered in code that already existed.
Important notice. What is described here is what a developer needs to know in order not to take naive decisions. In a real project, the dependency policy, vulnerability analysis and incident response are the responsibility of the organisation's security team or officer, with their own tools and processes. Your role is to keep the dependency tree under control, update when you are told to and not introduce dependencies without judgement. Application security practices are covered in 12-07.
- Auditing dependencies in practice
Three tools you will use from 11-05 onwards.
1. Seeing the whole tree. This is the first one and the one used most:
Output (trimmed) from a Spring Boot project with web:
[INFO] com.nexussoftware:bibliotech:jar:1.0.0-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.3.2:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:3.3.2:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:3.3.2:compile
[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.5.6:compile
[INFO] | | | | \- ch.qos.logback:logback-core:jar:1.5.6:compile
[INFO] | | | \- org.slf4j:jul-to-slf4j:jar:2.0.13:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:3.3.2:compile
[INFO] | | \- com.fasterxml.jackson.core:jackson-databind:jar:2.17.2:compile
[INFO] | \- org.springframework.boot:spring-boot-starter-tomcat:jar:3.3.2:compileThere you see at a glance three things this module is going to explain: that Spring Boot already brings Logback and SLF4J (11-07), that it already brings Jackson (11-07) and that it already brings embedded Tomcat (12-04). And you see that a jar you never declared, logback-core, is in your application.
To find who drags in a specific artefact:
2. Looking for known vulnerabilities. The OWASP plugin checks your tree against the public vulnerability database:
It generates an HTML report in target/ with the CVEs found and their severity. In a real project this runs in continuous integration, not by hand.
3. Seeing what is out of date.
It lists, dependency by dependency, which version you use and which is the latest available. Running it every few weeks and applying the patch updates is one of the most profitable hygiene practices there is.
- Which framework to learn first
If you have got this far, the list of options may be paralysing. The criterion for choosing the first one is not technical, it is about learning:
- Learn the one that is used most. Not because it is the best, but because when you get stuck —and you will— somebody has already got stuck the same way and written it down. Documentation, courses and answers are a real resource.
- Learn one well rather than three halfway. The concepts —IoC, DI, ORM, AOP, persistence context— transfer. The syntax does not. Someone who masters Spring learns Quarkus in two weeks; someone who has skimmed all three masters none.
- Learn with a real project, not with isolated examples. That is why this module is about BiblioTech, and why there is a whole module 12 to integrate it all.
- Why this course chooses Spring Boot
Honestly, three reasons and one warning.
- It is what the market has. A very large majority of Java back-end job openings mention Spring or Spring Boot. It is the knowledge with the highest immediate value.
- It covers every category on the map. With Spring Boot you touch DI, web, persistence, testing, configuration, security and observability with one coherent model. You learn eight things inside one ecosystem instead of across eight.
- Its magic is exactly the magic you already understand. Annotations, reflection, dynamic proxies. You are in the best possible position to learn it without it being magic.
The warning: Spring is not the answer to everything. For a small CLI it is a sledgehammer to crack a nut, and up to 12-03 you will see that a well-structured console application does not need it. For serverless functions with cold starts, Quarkus or Micronaut are better. And for a service that only runs three SQL queries, jOOQ or JdbcTemplate may fit better than Hibernate. Choosing well is part of the craft; and to choose you have to know.
- The plan for module 11
This module has a deliberate structure: each lesson settles one debt from module 10 and presents one tool completely and self-containedly. Integration into a real project is module 12.
graph TD
L1["11-01<br/>Introduction<br/>to frameworks"] --> L2["11-02<br/>Spring<br/>IoC, AOP, Boot"]
L2 --> L3["11-03<br/>Hibernate<br/>JPA over H2"]
L3 --> L4["11-04<br/>JUnit 5<br/>the first tests"]
L4 --> L5["11-05<br/>Maven<br/>building"]
L5 --> L6["11-06<br/>Mockito<br/>advanced testing"]
L6 --> L7["11-07<br/>Jackson, Lombok<br/>SLF4J and ecosystem"]
L7 --> M12["Module 12<br/>A real application"]
| Lesson | Tool | Debt it settles |
|---|---|---|
| 11-02 | Spring Framework and Spring Boot | The home-made SimpleContainer and per-environment configuration |
| 11-03 | Hibernate / JPA | CSV as persistence |
| 11-04 | JUnit 5 | The complete absence of tests |
| 11-05 | Maven | Hand-written javac and dependencies |
| 11-06 | Mockito | Testing what depends on network, disk and clock |
| 11-07 | Jackson, Lombok, SLF4J | JSON with indexOf, boilerplate code, java.util.logging |
By the end of the module, BiblioTech will be a Maven project with Spring, JPA persistence over H2, tests with JUnit and Mockito, JSON with Jackson and logging with SLF4J. It will not be a complete application yet —that is module 12— but all its pieces will be the real ones.
- Common Mistakes and Tips
Mistake: believing a framework excuses you from understanding what lies underneath. It is exactly the other way round. The framework automates; when the automation fails, only someone who understands the mechanism can fix it. If @Transactional does not roll back, you need to know about proxies and about database transactions.
Mistake: adding dependencies without evaluating them. "I need to sort a list, I'll add Guava." No. Look first at the JDK, then at what you already have in the tree, and only then at Maven Central.
Mistake: copying examples from the internet without checking the version. The unmistakable symptom: javax.persistence instead of jakarta.persistence, or WebSecurityConfigurerAdapter, which was removed. Those are from Spring Boot 2 and do not compile on 3. Always check the date and version of the example.
Mistake: pinning versions of dependencies the BOM manages. When you use spring-boot-starter-parent, do not put <version> on the dependencies it manages: you are taking away its job of guaranteeing that the versions are compatible with each other. This is explained in 11-05.
Mistake: using version ranges or LATEST. It destroys reproducibility: the same git tag compiles differently depending on the day. Always pin concrete versions.
Mistake: never updating "because it works". An application left untouched for three years accumulates dozens of known vulnerabilities. And when you finally have to update, the jump is so big that it becomes a project.
Tip: tell the core apart from the plumbing. Keep your domain (entities, business rules) as free of framework annotations as possible. BiblioTech's fine logic must be testable without starting Spring. This principle is explored further in 12-02.
Tip: learn to read a dependency:tree. It is the tool that will get you out of trouble most often: version conflicts, duplicate jars, classes appearing twice, and the security question "do I have this library?".
Tip: when something in the framework looks like magic, look for the mechanism. Ask yourself: is it an annotation read by reflection? Is it a proxy? Is it code generated at compile time? It is almost always one of the three, and then it stops being magic.
Tip: look at a starter's dependencies before adding it. A spring-boot-starter-web brings Tomcat, Jackson, validation and logging. Knowing that stops you adding Jackson yourself at another version and causing a conflict.
- Exercises
Exercise 1: classify and justify
For each item in the list, say whether it is a library or a framework, and justify your answer with the "who calls whom" criterion. In the ambiguous cases, explain why they are ambiguous.
java.util.Collections- JUnit 5
- Jackson's
ObjectMapper - Hibernate
- SLF4J
- Spring Boot
- The
SimpleContaineryou wrote in 10-03 - The
AuditProxyyou wrote in 10-03
Exercise 2: evaluating a real dependency
Nexus Software needs to generate barcodes for the labels on BiblioTech's books. A colleague proposes adding a library they found. Write the analysis you would carry out before accepting it, using the lesson's checklist, and decide. Available data:
groupId:artifactId—com.example:barcode-magic- Latest version:
0.4.1, published 3 years ago - Licence: not stated in the POM
- Usages in Maven Central: 7 projects
- Transitive dependencies: 14 jars, including a full graphics engine and an XML library
- Documentation: an 8-line README
- One medium-severity CVE open for 14 months
Exercise 3: the reverse map
For each of these "magic" behaviours you will see in the coming lessons, say which of the four mechanisms from section 6 makes it possible and what you wrote in module 10 that resembles it.
- You write
@ServiceonLoanManagerand Spring instantiates it on its own. - You write
@Transactionaland the database rolls back when an exception is thrown. - You write Lombok's
@Getterand agetTitle()you never wrote appears. - You declare
interface LoanRepository extends JpaRepository<Loan, Long>without implementing it and it works. - You write
@Testand the method runs without amain. - You write
@JsonProperty("isbn_13")and Jackson uses that name in the JSON.
Solutions
Solution 1
| Item | Type | Justification |
|---|---|---|
java.util.Collections |
Library | You call Collections.sort(...) when you want to. Control entirely yours. |
| JUnit 5 | Framework | There is no main. JUnit discovers your @Test methods by reflection and invokes them. Pure Hollywood Principle. |
Jackson's ObjectMapper |
Library | You call writeValueAsString(...). Even though it reads your annotations, you control the flow. |
| Hibernate | Both | It is a library when you call entityManager.find(...); it is a framework when automatic dirty checking detects that you modified a managed entity and issues an UPDATE without you calling anything. That second behaviour is inverted control (seen in 11-03). |
| SLF4J | Library (facade) | You call log.info(...). It is also a facade: the implementation is chosen at runtime, but the flow is still yours. |
| Spring Boot | Framework | It starts up, scans your classes, instantiates them, injects them, and calls your CommandLineRunner or your controller when a request arrives. |
Your SimpleContainer |
Framework (in miniature) | It scans @Component, decides the construction order and instantiates your classes. You do not call new: it does. |
Your AuditProxy |
Framework (in miniature) | It intercepts the call, runs code before and after, and decides when to invoke your method. The flow goes through it. |
The conclusion that matters: you wrote two miniature frameworks without calling them that.
Solution 2
Applying the checklist:
| Box | Result | Comment |
|---|---|---|
| Do I really need it? | Yes | Generating a correct barcode (EAN-13 checksums, quiet zones) is hard to do well by hand. It is a legitimate case for a dependency. |
| Licence compatible? | NO | With no declared licence, by default there is no permission to use it. This alone blocks the decision. |
| A release in the last 12 months? | NO | 3 years without a release. Dead for practical purposes. |
| Open CVEs? | NO | One of medium severity, unfixed for 14 months: it confirms the abandonment. |
| Reasonable transitivity? | NO | 14 jars, with a graphics engine and an XML library, to generate a barcode image. Disproportionate and an enormous attack surface. |
| Compatible with my Java? | Unknown | Version 0.x: the API can change without warning, though the abandonment makes that irrelevant. |
| A better alternative? | Yes | ZXing (Apache 2.0, from Google, maintained, very widely used) or Barcode4J are mature options for the same job. |
| Exit cost? | Low | It would be used from a single point, behind an interface of our own. It is the only positive. |
Decision: reject. Four critical boxes fail, and the licence alone would be enough. Counter-proposal: use ZXing, and do it behind an interface of our own (LabelGenerator), so that the specific library is isolated in a single class and the future cost of changing it is one implementation. And add dependency analysis to continuous integration so the next CVE is detected automatically.
A professional note: the licence point is not negotiated on technical grounds. In a company, a jar with no declared licence is a legal problem, and that conversation belongs to the appropriate person, not to the developer acting alone.
Solution 3
| Behaviour | Mechanism | Your module 10 equivalent |
|---|---|---|
1. @Service instantiates on its own |
Annotation + reflection. Spring scans the classpath, finds the annotated class, reads its constructor with getDeclaredConstructors(), resolves the parameters and calls newInstance |
The SimpleContainer with @Component, exactly the same loop (10-03) |
2. @Transactional rolls back |
Dynamic proxy. The object in the container is not your class: it is a wrapper that opens a transaction, invokes your method and commits or rolls back depending on whether there was an exception | AuditProxy and RetryProxy: the InvocationHandler that runs code before and after method.invoke(...) (10-03) |
3. Lombok's @Getter |
Code generation at compile time (annotation processor). There is no reflection and no proxy: the .class already contains the method. You can see it with javap |
The annotation processor studied in 10-02 |
4. JpaRepository with no implementation |
Dynamic proxy over an interface plus method-name analysis. Proxy.newProxyInstance generates an implementation on the fly that translates findByIsbn into a query |
Proxy.newProxyInstance over interfaces, exactly as you wrote it (10-03) |
5. @Test runs without a main |
Annotation + reflection. JUnit Platform discovers the classes, filters the methods with isAnnotationPresent(Test.class) and invokes them with Method.invoke |
The AnnotatedExporter, which walked members looking for an annotation and acted on it (10-03) |
6. @JsonProperty("isbn_13") |
Annotation + reflection. Jackson introspects the class, reads the field's annotation and uses its value as the JSON key | @CsvField("name") read by the AnnotatedExporter — it is literally the same design (10-02, 10-03) |
If you have been able to complete this table, the lesson's goal is met: there is no magic left, only four mechanisms you already know applied at industrial scale.
Conclusion
Frameworks stopped being a black box before you even used the first one.
You can tell a library from a framework by the only question that matters —who calls whom— and you recognise the Hollywood Principle when you see it: a @Test method with no main, a @Service that instantiates itself, an entity that saves without you calling save. And you understand inversion of control not as a textbook definition but as the concrete change you made in LoanManager: from creating its own dependencies with new to declaring them in the constructor and letting somebody else resolve them — with the four consequences that had (it can be tested, the implementation can be swapped, it can be configured per environment and the clock stops being the system's).
You know what you buy and what you pay. You buy non-differentiating code already solved, conventions any developer understands, an integrated ecosystem and —the most underestimated of all— security patches maintained by others. You pay a learning curve, architectural coupling, implicit behaviour that is hard to debug, and weight. And you know that the correct answer to "framework, yes or no?" always begins with "it depends on the context".
And above all, you know how the magic holds up: annotations with @Retention(RUNTIME) that are inert labels; reflection that reads them and acts; dynamic proxies that wrap your objects to add behaviour without touching your code —with the detail, which you will run into, that an internal call does not go through the proxy and that CGLIB cannot cope with anything final—; and code generation, at compile time with annotation processors or at runtime with bytecode. Four mechanisms. You have written or studied all four. And you have the complete map of what you did by hand → what the real framework does, which is both your road map for the module and your diagnostic manual when something breaks.
You have the map of the ecosystem sorted into eight categories with a default option in each, so that the endless list of names becomes eight decisions. And you have judgement for the decision you will take dozens of times: whether or not to add a dependency. With the prior question up front —doesn't the JDK already do it?, because java.time, HttpClient, Files.readString, String.isBlank and record have made many classic dependencies obsolete— and the real criteria behind it: maintenance, licence, adoption, transitivity, security and exit cost.
You know Maven Central, the groupId:artifactId:version coordinates, the predictable path inside ~/.m2 and semantic versioning with its most practical consequence today: the jump from Spring Boot 2 to 3 replaced javax.* with jakarta.*, and that is why an example copied from the internet with javax.persistence simply will not compile.
And you understand supply chain risk: that an average project has hundreds of jars nobody has reviewed running with your permissions, that Log4Shell proved the hard question was not patching but knowing what you have, and that the ability to update fast depends on exactly the two tools in this module —Maven to change a version with one line and JUnit to verify you broke nothing—. With the clear warning that, in a real project, dependency policy and vulnerability response belong to the security officer, and that application security practices arrive in 12-07.
The plan is set and every lesson settles a debt. The first one is the biggest: that hundred-and-fifty-line SimpleContainer you wrote so you would not have to call new, and which is missing absolutely everything else — scopes, lifecycle, profiles, externalised properties, ambiguity resolution and transactions.
In the next lesson, BiblioTech throws that container away and adopts the one ten thousand developers have spent twenty years polishing. You will see the ApplicationContext, the three forms of injection and why only one is advisable, the stereotypes, a bean's lifecycle, the scopes, the dev and prod profiles, the typed properties that replace your Configuration from 07-07, and Spring Boot's auto-configuration explained properly — not as magic, but as a set of conditions you can read and debug.
And when you get to @Transactional, you will not see a mysterious annotation: you will see your own AuditProxy, with twenty years of polish on top.
We start with Spring.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
