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 Initializr — start.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 documentation — docs.spring.io/spring-boot/docs/current/reference/html/. Always the authoritative source for the version you’re on.
- springdoc-openapi documentation — springdoc.org. Configuration, annotations, and migration notes from springfox.
- Testcontainers for Java — java.testcontainers.org. Containers for PostgreSQL, MySQL, Kafka, Redis, and just about anything else you might integrate with.
- Flyway documentation — flywaydb.org/documentation. Naming conventions, callbacks, and CI integration.
Happy bootstrapping. 🛠️