Career OS

Your First App, Demystified

In the next hour you’ll generate a project on a website, run one command, and have a production-grade web server answering HTTP on port 8080 — without writing a single line of code. Most beginners stop there, call it magic, and stay scared of it forever. You’re going to open the box and trace every one of those 3 seconds of startup, because the day this app fails to start in production, “it’s magic” is not an answer.

The Goal

By the end of this module you can:

  • Generate the SplitEase API project on start.spring.io and say what each of the five chosen dependencies actually pulls in
  • Explain every file in the generated skeleton, and why @SpringBootApplication is three annotations in a trenchcoat
  • Narrate the startup log line by line — context, component scan, auto-configuration, embedded Tomcat
  • De-magic auto-configuration into what it really is: if-statements over your classpath, and prove it with --debug
  • Configure the app with properties, profiles, and environment variables — and say why secrets never go in the file
  • Read the two classic startup failures (port in use, no database) without panic

The Lesson

start.spring.io — the project generator

Nobody hand-writes a Spring Boot project from scratch. start.spring.io (the “Spring Initializr”) generates the skeleton: build file, folder layout, main class, all version numbers already compatible with each other. That version compatibility is worth more than it sounds — pre-Boot, matching Spring, Hibernate, and Tomcat versions by hand was a real job hazard.

You’ll pick five dependencies for SplitEase. Know what each one actually drops onto your classpath, because — as you saw in module 01the classpath is what auto-configuration reads:

You tickWhat lands on the classpathWhy SplitEase needs it
Spring WebSpring MVC (controllers, routing), embedded Tomcat, Jackson (JSON)The REST API itself — module 03 onwards
Spring Data JPAHibernate (ORM), Spring Data (repository interfaces), HikariCP (connection pool)Talking to the database — module 04
ValidationHibernate Validator (the Bean Validation implementation)Rejecting bad requests cleanly — module 05
PostgreSQL DriverThe Postgres JDBC driver (runtime scope)The real database we commit to in module 04
H2 DatabaseA tiny in-memory database (runtime scope)A temporary stand-in so the app runs before Postgres exists

Notice what you did not tick: nothing called “JSON support”, “server”, or “connection pool”. They ride along inside the starters. A starter is just a dependency list with a name — spring-boot-starter-web is an empty jar whose only job is to drag in Spring MVC, Tomcat, and Jackson together, pre-matched.

The skeleton, file by file

Unzip the download and you get this:

splitease-api/
├── mvnw, mvnw.cmd                # Maven wrapper — runs Maven without installing Maven
├── pom.xml                       # the shopping list
└── src/
    ├── main/
    │   ├── java/com/splitease/
    │   │   └── SpliteaseApiApplication.java   # the only Java file. One.
    │   └── resources/
    │       ├── application.properties          # configuration — empty for now
    │       ├── static/                         # (for files served as-is; ignore)
    │       └── templates/                      # (for server-side HTML; ignore)
    └── test/java/com/splitease/
        └── SpliteaseApiApplicationTests.java   # one test: "does the context start?"

mvnw is the Maven wrapper — a small script that downloads the exact Maven version the project wants and runs it. This is why every command in this track is ./mvnw something and never mvn something: everyone who clones the repo builds with the identical tool, no “works on my machine”.

pom.xml is the shopping list. Three parts matter:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.4.5</version>   <!-- your generated version may differ; 3.x is what matters -->
</parent>

The parent is where all the version numbers live. That’s why your <dependencies> block has no <version> tags — the parent pins a tested-together set for every library Boot knows about. Then the dependencies themselves (your five starters), then the spring-boot-maven-plugin, which is what makes ./mvnw spring-boot:run exist and what later packages the app into one runnable jar.

SpliteaseApiApplication.java is eleven lines:

package com.splitease;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpliteaseApiApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpliteaseApiApplication.class, args);
    }
}

A plain main — the same public static void main from Core Java 01. No server config, no XML, no deployment descriptor. One annotation carries everything.

@SpringBootApplication — three annotations in a trenchcoat

Open the source of @SpringBootApplication (Ctrl+click it in your IDE) and you find it’s a shortcut for three real annotations stacked together:

Inside the trenchcoatWhat it actually does
@SpringBootConfigurationMarks this class as a configuration class — a place where you could define beans with @Bean methods. It is @Configuration wearing a Boot badge.
@EnableAutoConfigurationSwitches on the if-statement machine: examine the classpath, create the beans the evidence calls for. The whole next section.
@ComponentScan”Scan this class’s package and everything below it for @Component, @Service, @RestController… and register them as beans.”

That last one explains a bug you will absolutely hit one day: @ComponentScan starts from the package of this class, downward. Put a controller in com.splitease.web — found. Put it in com.other.web — silently invisible, no error, your endpoint just 404s. Keep the main class at the root package and everything below it; that’s the whole convention.

The 3 seconds of startup, narrated

Run ./mvnw spring-boot:run and the log tells a story. Here is the story with subtitles:

flowchart TD
    A["Banner prints - Spring Boot and Java versions"] --> B["ApplicationContext created - the container from module 01, empty"]
    B --> C["Component scan - walk com.splitease and below, collect annotated classes"]
    C --> D["Auto-configuration - run the classpath if-statements, add Boot's beans"]
    D --> E["Build and wire all beans in dependency order"]
    E --> F["Start embedded Tomcat on port 8080"]
    F --> G["Log line - Started SpliteaseApiApplication in about 3 seconds"]

Connect this to module 01: the container story you stepped through there is stages B through E here. What Boot adds on top is stage D — beans you never wrote, conjured from classpath evidence — and stage F, a real web server started inside your JVM as just another bean.

Find these lines in your own log and you’ve internalised it:

  • Tomcat initialized with port 8080 (http) — stage F beginning
  • HikariPool-1 - Starting... — the JPA starter found a database (H2, for now) and opened a connection pool
  • Started SpliteaseApiApplication in 2.9 seconds — the container is live; main has done its job

Auto-configuration de-magicked — it’s if-statements

Here is the entire trick. Spring Boot ships a few hundred small configuration classes, each guarded by conditions. The one that gives you Tomcat looks like this (trimmed from the real ServletWebServerFactoryAutoConfiguration):

@AutoConfiguration
@ConditionalOnClass(Tomcat.class)                          // IF Tomcat is on the classpath
class TomcatAutoConfig {

    @Bean
    @ConditionalOnMissingBean(ServletWebServerFactory.class)  // AND you didn't define your own
    TomcatServletWebServerFactory tomcatFactory() {
        return new TomcatServletWebServerFactory();            // THEN create one
    }
}

Read it as plain English: if Tomcat is on the classpath, and the developer hasn’t defined their own server bean, create a Tomcat one. That’s it. Hundreds of these run at startup. There is no code generation, no bytecode rewriting, no magic — a pile of if-statements evaluated against your jar list and your existing beans.

Two consequences worth tattooing on:

  1. Adding a dependency changes behaviour. Drop the H2 jar in, and a datasource appears. That’s not spooky — a condition that was false became true.
  2. You always win. @ConditionalOnMissingBean means every Boot default backs off the moment you define your own bean of that type. Boot fills gaps; it never overrules you.

And you can watch the if-statements run. Add one line to application.properties:

debug=true

Restart, and the log prints the CONDITIONS EVALUATION REPORT — every auto-configuration class under Positive matches (conditions true, bean created) or Negative matches (with the exact condition that failed). When someone asks “where did this bean come from?”, this report is the answer, on paper.

application.properties, profiles, and where secrets live

application.properties is the app’s settings file. Key=value:

spring.application.name=splitease-api
server.port=8080

But dev and prod need different settings — H2 locally, Postgres in production. Profiles solve this: application-dev.properties and application-prod.properties sit beside the main file, and spring.profiles.active=dev decides which overlay applies on top of the defaults.

Now the rule that separates professionals from leaked-credentials news stories: secrets never go in any properties file. These files are committed to git — and this repo is public. A password committed once lives in git history forever, even after you delete it. The professional pattern:

# application-prod.properties — committed, no secret in sight
spring.datasource.password=${DB_PASSWORD}

The ${DB_PASSWORD} placeholder reads an environment variable at startup — set on the server, never in the repo. Spring’s relaxed binding even maps env vars directly: the variable SPRING_DATASOURCE_PASSWORD overrides the property spring.datasource.password with no placeholder needed. The file documents what is configured; the environment supplies the value.

Embedded server — one line of history

The old world: you built a .war file and deployed it into a separately installed, separately configured Tomcat that an ops team owned, and “works on my Tomcat” was a genuine dispute. The embedded model inverts it: the server is a library inside your jar, so java -jar splitease-api.jar is the entire deployment — same server, same version, laptop and production. Docker sealed the argument: one self-contained jar fits a container perfectly, which is exactly what Month 3 does with this app.

Actuator — the app’s pulse

One more starter, spring-boot-starter-actuator, gives the app operational endpoints. The one that matters today is /actuator/health, which returns:

{"status":"UP"}

Trivial-looking, load-bearing: load balancers, Kubernetes probes, and uptime monitors all decide whether your app gets traffic by polling exactly this. You’ll add it by hand in Build This — editing pom.xml yourself, once, so you feel that a starter is just a dependency entry that auto-configuration reacts to.

Reading a startup failure

Spring Boot startup failures come with a FailureAnalyzer — a plain-English diagnosis printed after the stack trace. The two classics:

Port already taken:

***************************
APPLICATION FAILED TO START
***************************

Description:

Web server failed to start. Port 8080 was already in use.

Action:

Identify and stop the process that's listening on port 8080 or configure this
application to listen on another port.

Nothing is wrong with your code — another process holds the port. Find it (netstat -ano | findstr 8080 on Windows) or change server.port.

Database that isn’t there: point the app at a Postgres that isn’t running and Hibernate fails while trying to connect at startup. Buried in the trace: Connection to localhost:5432 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections. The skill from Core Java 01 applies unchanged: first line for what, scan for the deepest Caused by: for why. Connection refused always means “nothing is listening there” — a missing server, not broken code.

Check The Concept

How This Shows Up At Work

  • The Friday deploy that won’t start. Production logs show Port 8080 was already in use — the old instance didn’t shut down cleanly. The engineer who reads the FailureAnalyzer fixes it in two minutes; the one who reruns the deploy five times pages the whole team.
  • “Where is this bean even coming from?” A teammate hits behaviour nobody wrote. You add debug=true, open the conditions report, and point at the auto-configuration and the exact condition that fired. That move earns instant credibility.
  • The leaked secret. A real and depressingly common incident: database password committed in application.properties, repo later made public, bots find it within hours. Knowing why secrets live in env vars — git history is forever — is the difference between knowing the rule and being the postmortem.
  • Interview staple: “What does @SpringBootApplication do?” Most candidates say “it starts the app.” You name the three annotations and what each contributes, then explain auto-configuration as conditional beans. That answer changes the interview’s temperature.
  • The invisible controller. A code review moves a class to a “better” package outside the root, every test still compiles, and the endpoint 404s in staging. You spot it from the package name alone, because you know where component scan starts.

Build This

Where you are: the Core Java track ended with SplitEase CLI — Friend, Expense, Ledger, settle-up, all in a terminal app. Module 01 gave you the container mental model. Nothing of the API exists yet. This module creates the project every remaining module builds on.

  1. Go to start.spring.io and set exactly:

    • Project: Maven · Language: Java · Spring Boot: latest stable 3.x
    • Group: com.splitease · Artifact: splitease-api · Name: splitease-api · Package name: com.splitease
    • Packaging: Jar · Java: 21
    • Dependencies — add all five: Spring Web, Spring Data JPA, Validation, PostgreSQL Driver, H2 Database
  2. Click Generate, unzip into your workspace, and make it a repo:

cd splitease-api
git init
git add .
git commit -m "[project]: splitease-api - generated skeleton from start.spring.io"
  1. Before running anything, read what you got. Open pom.xml and find your five dependencies plus the parent. Open SpliteaseApiApplication.java and Ctrl+click @SpringBootApplication to see the trenchcoat with your own eyes.

  2. Run it (use .\mvnw.cmd in PowerShell, ./mvnw in Git Bash):

./mvnw spring-boot:run

Read the log against the startup diagram: find the banner, the Tomcat initialized with port 8080 line, the HikariPool-1 - Starting line (that’s H2 — no Postgres exists yet, and the JPA starter quietly fell back to the in-memory database it found on the classpath), and the final Started SpliteaseApiApplication.

  1. Hit it: open http://localhost:8080 in a browser. You get the Whitelabel Error Page with a 404 — and that’s correct. The server is alive; you just have zero endpoints. Module 03 fixes that.

  2. Add actuator by hand — open pom.xml and add inside <dependencies>:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

No version tag — the parent supplies it. Restart the app, then:

curl http://localhost:8080/actuator/health

Expected:

{"status":"UP"}

Sit with what just happened: you added one dependency, and working HTTP endpoints appeared. Classpath changed → conditions changed → beans changed. Auto-configuration, observed in the wild.

  1. Watch the decisions. Add debug=true to application.properties, restart, and find the CONDITIONS EVALUATION REPORT in the log. Locate DataSourceAutoConfiguration under Positive matches. Remove the line after.

  2. Break it on purpose #1 — port conflict. In application.properties set server.port=80, restart. On most Windows machines a system service already holds port 80, so you get Web server failed to start. Port 80 was already in use. — read the full Description/Action block. (If port 80 happens to be free on your machine, revert it and instead start a second copy of the app in another terminal — same failure on 8080.) Revert before moving on.

  3. Break it on purpose #2 — phantom database. Add to application.properties:

spring.datasource.url=jdbc:postgresql://localhost:5432/splitease
spring.datasource.username=splitease
spring.datasource.password=splitease

Restart. Startup fails — scroll to the deepest Caused by: and find Connection to localhost:5432 refused. Nothing is listening on 5432 because you haven’t installed Postgres. Read the whole trace once, slowly; this exact failure will greet you in real jobs whenever a database is down, misconfigured, or firewalled.

  1. Park it. Delete those three lines (or comment them with #). The app falls back to H2 and starts clean — that is the official temporary arrangement until module 04 stands up real PostgreSQL. Final check, then commit:
curl http://localhost:8080/actuator/health
git add .
git commit -m "[project]: splitease-api - actuator health green, H2 placeholder until module 04"

Where to Practice

ResourceWhat to do thereHow long
start.spring.ioRegenerate the project with only Spring Web ticked, diff the two pom.xml files — see exactly what each starter adds15 min
spring.io/guidesDo the “Building an Application with Spring Boot” guide — same flow, second rep on a throwaway project30 min
docs.spring.ioSpring Boot reference → Core Features → skim “Auto-configuration” and “Externalized Configuration” — knowing where these pages live is the skill20 min
BaeldungRead the free “Spring Boot Starters” article — survey of the starter catalogue15 min

Check Yourself

  1. Name the three annotations inside @SpringBootApplication and give each one’s job in one line.
  2. What does spring-boot-starter-web actually contain, and what does it pull in?
  3. Walk the startup sequence from banner to “Started” in five steps.
  4. In plain English, state the if-statement that gives you an embedded Tomcat.
  5. How do you see every auto-configuration decision Boot made, with reasons?
  6. Why do committed properties files never hold secrets, and what holds them instead?
  7. Your app fails with “Port 8080 was already in use.” Is your code broken? What do you do?
  8. Why is the H2 dependency in the project if PostgreSQL is the real database?
Answers
  1. @SpringBootConfiguration — marks the class as a bean-defining configuration class. @EnableAutoConfiguration — turns on the classpath if-statements that register Boot’s default beans. @ComponentScan — scans this package and below for annotated classes to register as beans.
  2. Almost nothing itself — a starter is a named dependency list. It pulls in Spring MVC, embedded Tomcat, and Jackson, with versions pre-matched by the Boot parent.
  3. Banner prints → ApplicationContext (the container) is created → component scan collects your annotated classes → auto-configuration runs its conditions and adds Boot’s beans → beans are built and wired, then embedded Tomcat starts on 8080 and “Started” logs.
  4. IF Tomcat is on the classpath AND no server bean is already defined, THEN create a Tomcat server bean. Auto-configuration is hundreds of these evaluated at startup.
  5. Set debug=true (or run with --debug) and read the CONDITIONS EVALUATION REPORT — positive matches with conditions met, negative matches with the exact failed condition.
  6. Properties files are committed, and git history keeps every value forever even after deletion — fatal in a public repo. Secrets live in environment variables on the server; the file references them as placeholders like ${DB_PASSWORD}.
  7. Code is fine — another process owns the port. Find and stop it (netstat -ano | findstr 8080) or change server.port. The FailureAnalyzer’s Action line says exactly this.
  8. It’s the explicit temporary stand-in: an in-memory database that lets the app boot and run before real Postgres is set up in module 04. The JPA starter auto-configures it because it’s on the classpath and no datasource URL is set.

Explain it out loud: Run ./mvnw spring-boot:run and narrate the scrolling log to an empty chair, line by line — what the banner means, when the container exists, what component scan found, why Tomcat starts even though you never wrote server code, and what HikariPool is connecting to. If any line makes you mumble, that’s the section to reread.

Still Unclear?

Copy-paste any of these into Claude — they deepen understanding, they don’t do the work:

I ran a Spring Boot app with debug=true and got the CONDITIONS EVALUATION
REPORT. Here are five lines from my Positive matches: [paste them].
Explain each condition in plain English, then quiz me: give me three made-up
condition lines and ask me to predict whether the bean gets created.
Explain @ConditionalOnClass and @ConditionalOnMissingBean as if-statements,
then walk me through what changes at startup when I (a) remove the H2
dependency from pom.xml and (b) define my own DataSource bean. Don't write
code for me - make me predict each outcome first, then check me.
My Spring Boot app fails at startup and here is the stack trace: [paste].
Don't tell me the fix. Teach me to find it: which line states the problem,
where the deepest Caused by is, and what the FailureAnalyzer block adds.
Then ask me what I think the fix is and critique my answer.

Why AI Can’t Do This For You

AI can generate this entire project — it’s literally what start.spring.io does without AI. But when startup fails on a real server at 11pm — port held by a zombie process, datasource pointing at a firewalled host, a bean missing because someone moved a package — the fix requires reading your log against a mental model of your startup sequence. AI hasn’t seen your log, your server, or what changed in yesterday’s merge. The person who can narrate those 3 seconds finds it; the person who calls it magic files a ticket and waits.

Auto-configuration knowledge compounds, too. Every “weird Spring behaviour” you’ll ever debug — a bean that exists in dev but not prod, a property that’s mysteriously ignored, two datasources fighting — is a conditions question. Learn to read the decisions once and an entire category of “magic” becomes ordinary if-statements you can check.

Module done? Add it to today’s tracker

Saves your progress on this device.