Starting a Spring Boot API Microservice From Scratch With Spring Initializr

The fastest way to get a new Java microservice off the ground is also the most boring one, and that’s a compliment. You go to start.spring.io, click a few checkboxes, download a zip, and you have a runnable Hypertext Transfer Protocol (HTTP) service in under five minutes. No archetype incantations, no copy-pasting a pom.xml from a colleague’s repo, no “wait, which version of Spring Cloud goes with which Boot?” — Initializr handles compatibility for you. ☕

What We’re Building

A small, API-laden microservice — call it orders-service. It exposes a handful of Representational State Transfer (REST) endpoints, talks Java Script Object Notation (JSON), persists to a database, validates inputs, exposes health checks, and publishes an interactive Swagger / OpenAPI page that lets you try the endpoints from a browser. The goal isn’t a particular business problem — it’s a clean skeleton you can fork the next time someone says “we need a new service for X.”

Generate the Project

Head to https://start.spring.io. The form is small but every choice matters, so let’s walk through it.

  • Project: Maven. Gradle is fine too; pick whichever your team already uses. The rest of this post shows Maven snippets.
  • Language: Java.
  • Spring Boot version: the latest non-snapshot release. Avoid the SNAPSHOT and M (milestone) entries — they’re moving targets.
  • Group / Artifact / Name: com.acme, orders-service, orders-service. Group becomes the base package; artifact becomes the JAR name.
  • Packaging: Jar. Wars only make sense if you’re deploying into an existing servlet container, which you shouldn’t be.
  • Java version: the latest Long-Term Support (LTS) release available — 17 at time of writing. Stay on LTS for production.

Then the dependency list. For an API-laden service, click the Add Dependencies button and pick:

  • Spring Web — REST controllers, embedded Tomcat, JSON via Jackson.
  • Spring Data JPA — repository abstraction over the database.
  • PostgreSQL Driver — or your database of choice (MySQL Driver, H2 for in-memory testing).
  • Validation — Jakarta Bean Validation (the @Valid, @NotNull, @Size annotations).
  • Spring Boot Actuator — health, metrics, info endpoints.
  • Spring Boot DevTools — auto-restart on classpath changes during local dev.
  • Lombok — kill the getter/setter/builder boilerplate.
  • Testcontainers — run a real PostgreSQL in tests instead of mocking the repository layer.

Click Generate. You get a zip. Unzip it, cd in, and you can already run the service:

1
2
unzip orders-service.zip && cd orders-service
./mvnw spring-boot:run

That’s it. You’ll see Tomcat start on port 8080 and the embedded actuator endpoints come alive. 🎉

Add Swagger / OpenAPI Manually

Initializr doesn’t ship a Swagger checkbox, so add it by hand. The community-maintained library that plays nicely with current Spring Boot is springdoc-openapi. In your pom.xml:

1
2
3
4
5
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.3.0</version>
</dependency>

Restart the app and visit http://localhost:8080/swagger-ui.html. The library scans your @RestController classes, reads their annotations, and generates an interactive page where you can click Try it out and fire real requests. The raw OpenAPI 3 spec is served at /v3/api-docs in JSON, which is what you give to consumers who want to generate clients. 📘

Avoid the older springfox library you’ll find in many Stack Overflow answers — it has not kept up with current Spring Boot and you’ll fight startup errors. springdoc-openapi is the answer.

A Minimal Working Endpoint

Initializr drops a OrdersServiceApplication.java with the @SpringBootApplication annotation. Add an entity, a repository, a Data Transfer Object (DTO), and a controller next to it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    private String customerEmail;

    @NotNull @Positive
    private BigDecimal amount;

    private Instant createdAt = Instant.now();
}
1
2
3
public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByCustomerEmail(String customerEmail);
}
1
2
3
4
public record CreateOrderRequest(
    @NotBlank @Email String customerEmail,
    @NotNull @Positive BigDecimal amount
) {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {

    private final OrderRepository repository;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Order create(@Valid @RequestBody CreateOrderRequest request) {
        return repository.save(new Order(
            null, request.customerEmail(), request.amount(), Instant.now()
        ));
    }

    @GetMapping("/{id}")
    public Order get(@PathVariable Long id) {
        return repository.findById(id)
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }

    @GetMapping
    public List<Order> list(@RequestParam(required = false) String customerEmail) {
        return customerEmail == null
            ? repository.findAll()
            : repository.findByCustomerEmail(customerEmail);
    }
}

That’s a real, validated, persistent endpoint in under fifty lines. Lombok handles the boilerplate, Spring Data writes the SQL, and Jakarta Validation rejects bad payloads before they ever reach your code.

Wire Up the Database

The default application.properties Initializr drops is empty. Add database settings, but read them from environment variables so you can deploy the same artifact to multiple environments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring.application.name=orders-service
server.port=${PORT:8080}

spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/orders}
spring.datasource.username=${DB_USER:orders}
spring.datasource.password=${DB_PASSWORD:orders}

spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.jdbc.time_zone=UTC

management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.probes.enabled=true

springdoc.swagger-ui.path=/swagger-ui.html
sprindoc.api-docs.path=/v3/api-docs

Two important defaults to flag:

  • spring.jpa.hibernate.ddl-auto=validate — validates that the schema matches your entities at startup, but does not create or modify tables. Use a real migration tool (Flyway or Liquibase) for that.
  • management.endpoint.health.probes.enabled=true — exposes /actuator/health/liveness and /actuator/health/readiness, which are exactly what Kubernetes wants. 🐳

Add Flyway for Migrations

Add to pom.xml:

1
2
3
4
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>

Then create src/main/resources/db/migration/V1__create_orders.sql:

1
2
3
4
5
6
7
8
CREATE TABLE orders (
    id              BIGSERIAL PRIMARY KEY,
    customer_email  VARCHAR(255) NOT NULL,
    amount          NUMERIC(12, 2) NOT NULL CHECK (amount > 0),
    created_at      TIMESTAMP NOT NULL DEFAULT now()
);

CREATE INDEX idx_orders_customer_email ON orders(customer_email);

Flyway runs on app startup, applies any pending migrations, and refuses to start if a previously-applied migration’s checksum has changed. That last property is the single most useful safety net in Java backend development — it makes “I changed the migration after it was applied somewhere” loud instead of silent. 🛡️

A Quick Test With Testcontainers

Real database tests are a hill worth dying on. @DataJpaTest with the in-memory H2 driver tests Hibernate’s idea of your database, not the actual database. Testcontainers spins up a real PostgreSQL in Docker, points your test at it, and tears it down when you’re done:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@SpringBootTest
@Testcontainers
class OrderControllerTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url",      postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired private TestRestTemplate restTemplate;

    @Test
    void rejectsInvalidPayload() {
        var response = restTemplate.postForEntity(
            "/api/orders",
            Map.of("customerEmail", "not-an-email", "amount", -5),
            String.class
        );
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
    }
}

Slow on first run (Docker pulls the image), fast on every subsequent run.

Dockerize It

Spring Boot has built-in image building. No Dockerfile needed:

1
2
./mvnw spring-boot:build-image \
    -Dspring-boot.build-image.imageName=acme/orders-service:0.1.0

That uses Cloud Native Buildpacks under the hood to produce a small, layered image you can docker run immediately. If you’d rather hand-roll a Dockerfile for fine-grained control, the multi-stage version is:

1
2
3
4
5
6
7
8
9
10
FROM eclipse-temurin:17-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw -B package -DskipTests

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/orders-service-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

The Final Library Cheat Sheet

If you’re starting a new API-laden Spring Boot service today and you want a sensible default stack, pick these:

  • spring-boot-starter-web — REST + embedded Tomcat.
  • spring-boot-starter-data-jpa + the relevant JDBC driver — persistence.
  • spring-boot-starter-validation — request validation.
  • spring-boot-starter-actuator — health, metrics, probes.
  • springdoc-openapi-starter-webmvc-ui — Swagger UI + OpenAPI 3 spec.
  • lombok — boilerplate killer.
  • flyway-core — versioned schema migrations.
  • testcontainers (postgresql, junit-jupiter) — real-database integration tests.
  • spring-boot-starter-security — when you need authentication (often the next thing you’ll add).

Closing Thoughts

Spring Initializr does its best work when you treat it as a starting point, not a destination. Generate the project, run it once to confirm it boots, then add the one or two libraries Initializr doesn’t include (Swagger, sometimes Flyway), wire up a real database, and you’re at the point where you can start writing actual business logic. The whole arc from “empty browser tab” to “first endpoint live in Docker” is comfortably under an afternoon. 💡

Further Reading

  • Spring Initializrstart.spring.io. The actual tool. The web version, the IDE integrations (IntelliJ, VS Code), and the underlying REST API all live here.
  • Spring Boot reference documentationdocs.spring.io/spring-boot/docs/current/reference/html/. Always the authoritative source for the version you’re on.
  • springdoc-openapi documentationspringdoc.org. Configuration, annotations, and migration notes from springfox.
  • Testcontainers for Javajava.testcontainers.org. Containers for PostgreSQL, MySQL, Kafka, Redis, and just about anything else you might integrate with.
  • Flyway documentationflywaydb.org/documentation. Naming conventions, callbacks, and CI integration.

Happy bootstrapping. 🛠️

This entry was posted in java and tagged , , , . Bookmark the permalink.

Comments are closed.