1. What “Setup” Really Means in a Real Java Team
Project setup is not only about creating a folder and compiling a Hello World. Setup means establishing a repeatable baseline: dependency versions that won’t conflict, plugins that produce consistent artifacts, test tooling that doesn’t break on CI, and conventions that make code reviews predictable. It also means defining how you add features later. That “later” part is the trap: many setups look great on day one and become fragile when you introduce security, metrics, tracing, database migrations, or multi-module builds.
When a team says “we need a faster way to start projects,” they usually mean two things. First, “I want a working project in minutes.” Second, “I want projects to remain consistent after six months of changes.” Spring Boot Starters and Maven Archetypes solve these two needs differently.
1.1 Spring Boot Starters: Composing Capabilities Instead of Copying Configuration
A Spring Boot Starter is essentially a curated dependency set that enables a capability: web, security, data access, validation, actuator, messaging, and so on. The magic is not just “it adds jars.” Starters typically work together with Spring Boot’s auto-configuration and dependency management so that you don’t have to micro-manage versions and wiring for common integrations.
When I add spring-boot-starter-web, I’m not just getting Spring MVC. I’m getting a bundle of dependencies that play nicely together, with sensible defaults and predictable behavior. When I add spring-boot-starter-test, I’m not manually deciding which JUnit engine or assertion library to use first. Boot sets a baseline that most teams can live with, and it stays consistent across upgrades.
The biggest hidden benefit is that starters scale with change. Your project can evolve by adding or removing capabilities in a controlled way—without re-laying a foundation every time.
1.2 Maven Archetypes: Stamping a Skeleton at the Beginning
A Maven Archetype is a project template generator. It produces an initial structure: packages, a sample class, a POM, maybe a README, maybe a basic CI file. It’s like using a cookie cutter. You run a command, a repo appears, and it compiles.
Archetypes shine when you want consistent scaffolding across many projects, especially when you’re not using Spring Boot, or when you need a very specific multi-module structure and naming convention from the first commit. But the archetype itself does not “stay connected” to the generated project. Once the skeleton is created, it’s just files in Git. If you want to improve the template later, you don’t automatically upgrade old projects. The archetype is a one-time stamp, not a living mechanism.
That’s why archetypes are great at starting, but weak at evolving.
1.3 The Mental Model That Makes the Difference Obvious
Here’s the simplest way I think about it.
A Spring Boot Starter is like choosing a standardized Lego set where pieces are designed to fit and you can keep adding sets later without rebuilding your house. A Maven Archetype is like printing a blueprint and building a house from it. You can modify the house later, but the blueprint doesn’t update your house automatically, and your modifications may drift away from whatever “standard” the blueprint intended.
If your team keeps creating new services and wants them to behave consistently over time—observability, error handling, logging, security defaults—starters are the strategic tool. If your team’s pain is “everyone keeps starting with different folder structures and bad POMs,” archetypes can help at the earliest stage.
2. Comparing Starters and Archetypes Where It Actually Hurts
Most comparisons stop at “starters are dependencies, archetypes are templates.” That’s true but not useful. The real difference shows up in maintenance, upgrades, onboarding, and platform consistency.
2.1 Maintenance and Upgrade Story: Living Baselines vs Fossilized Templates
With starters, you can upgrade Spring Boot, and the dependency ecosystem upgrades with you in a coordinated way. Boot’s dependency management is a huge part of why teams trust it: fewer version conflicts, fewer “works on my machine” surprises, and fewer random CVE fixes where you have to chase transitive dependencies manually.
With archetypes, maintenance is your responsibility in every generated project. If your archetype created a POM that pins a plugin version that later becomes problematic, every project carries that baggage until you fix it—manually, repo by repo. You can improve the archetype for future projects, but old ones remain stuck. In big organizations, that becomes version drift, which becomes security drift, which becomes “why does service A behave differently than service B?”
2.2 Team Consistency: How You Enforce Standards Without Becoming a Template Police
Archetypes enforce standards at birth. That’s nice, but it also means standards are easiest to break right after generation: someone edits the POM, changes plugin configs, deletes modules, or adds ad-hoc dependencies. A year later you have “the standard,” plus 27 variations of the standard.
Starters enforce standards by being part of the build graph. You can create internal starters that include consistent defaults—logging format, correlation IDs, actuator exposure, security baseline, exception mapping—and then every service opts into that capability by depending on the starter. When you improve the starter version, projects upgrade like they would upgrade any library. That’s a living standard.
2.3 Developer Experience: How Fast You Go from “Zero” to “Feature Work”
If I’m building a Spring service, Spring Initializr plus starters usually gets me to “real feature work” faster than an archetype. Because starters reduce wiring and dependency juggling. An archetype might generate more files up front, but a lot of those files become irrelevant or wrong the moment I add real features.
Archetypes are still useful when you need non-trivial multi-module layout from the start, or when you’re generating a non-Spring app, or when you want a company-specific repo layout that includes docs, scripts, CI templates, and governance files.
3. A Concrete Java Example: “Feature by Dependency” Using Spring Boot Starters
I want to show the difference in a way that feels real. So I’ll build a tiny service that exposes an endpoint and includes validation, structured error responses, and health checks. The point is not the endpoint. The point is how little glue code we write when we let starters and auto-configuration do the heavy lifting.
3.1 The Maven pom.xml Using Starters (Small, Focused, Maintainable)
In a Spring Boot project, I typically start with a parent that manages versions, then add starters for capabilities. Here’s a clean baseline.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelversion>4.0.0</modelversion>
<parent>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-parent</artifactid>
<version>3.3.4</version>
<relativepath>
</relativepath></parent>
<groupid>com.sayphim.demo</groupid>
<artifactid>starter-vs-archetype</artifactid>
<version>1.0.0</version>
<name>starter-vs-archetype</name>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Web capability -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
<!-- Bean validation capability -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-validation</artifactid>
</dependency>
<!-- Operational capability: /actuator/health, metrics hooks, etc -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-actuator</artifactid>
</dependency>
<!-- Testing capability -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-test</artifactid>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-maven-plugin</artifactid>
</plugin>
</plugins>
</build>
</project>
This POM looks almost too simple, and that’s the point. I’m expressing intent: “I want web, validation, actuator, tests.” I’m not hand-picking 14 dependencies and hoping they don’t fight.
Now let’s implement an endpoint that benefits directly from those starters.
3.2 The Java API: Validation and Clean Errors Without Extra Plumbing
I’ll create a request object with validation rules, a controller, and a global exception handler that turns validation errors into a predictable JSON structure. Notice how the starter approach shapes code: it’s less about wiring and more about behavior.
package com.sayphim.demo.api;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public class RateMovieRequest {
@NotBlank(message = "movieId must not be blank")
private String movieId;
@Min(value = 0, message = "score must be between 0 and 10")
@Max(value = 10, message = "score must be between 0 and 10")
private int score;
public RateMovieRequest() {}
public RateMovieRequest(String movieId, int score) {
this.movieId = movieId;
this.score = score;
}
public String getMovieId() {
return movieId;
}
public int getScore() {
return score;
}
public void setMovieId(String movieId) {
this.movieId = movieId;
}
public void setScore(int score) {
this.score = score;
}
}
This works because spring-boot-starter-validation brings in Jakarta Validation and integrates it into Spring MVC automatically. I didn’t configure a validator bean or add special filters. I just declare constraints and use @Valid in the controller.
package com.sayphim.demo.api;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.util.Map;
@RestController
@RequestMapping("/api/movies")
public class MovieRatingController {
@PostMapping("/rate")
public ResponseEntity<map<string, object="">> rate(@Valid @RequestBody RateMovieRequest req) {
// Imagine saving to DB here. For demo purposes, return a structured response.
return ResponseEntity.ok(Map.of(
"movieId", req.getMovieId(),
"score", req.getScore(),
"ratedAt", Instant.now().toString(),
"message", "Thanks! Your rating was recorded."
));
}
}
At this point, if someone sends a bad payload like an empty movieId or a score of 99, Spring will throw a validation exception before hitting the handler logic. That’s already a huge reduction in boilerplate: you’re not writing manual if-checks everywhere, which is where bugs and inconsistencies breed.
Now I’ll make those validation errors look professional.
package com.sayphim.demo.api;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
@RestControllerAdvice
public class ApiErrorHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<map<string, object="">> handleValidation(MethodArgumentNotValidException ex) {
Map<string, string=""> fieldErrors = new LinkedHashMap<>();
for (FieldError fe : ex.getBindingResult().getFieldErrors()) {
fieldErrors.put(fe.getField(), fe.getDefaultMessage());
}
Map<string, object=""> body = Map.of(
"timestamp", Instant.now().toString(),
"status", HttpStatus.BAD_REQUEST.value(),
"error", "Validation failed",
"fields", fieldErrors
);
return ResponseEntity.badRequest().body(body);
}
}
This is where the starter approach quietly pays off. Spring MVC already knows how to bind JSON into Java objects, run validation automatically, and throw structured exceptions. I only decide how to format the response. In other words, I’m controlling the API contract, not fighting the framework.
3.3 The Actuator Win: Operational Endpoints Without “Ops Homework”
Because I included spring-boot-starter-actuator, I can add basic health endpoints that tooling understands. In application.yml, I can expose a minimal safe set.
management:
endpoints:
web:
exposure:
include: health,info
Now /actuator/health exists without me writing a controller. This is the kind of thing archetypes often try to pre-generate, but starters make it an opt-in capability with consistent behavior across services.
3.4 A Small Test That Shows Starter-Based Setup Produces Predictable Testing Defaults
Testing is another hidden tax of setup. With spring-boot-starter-test, you get a curated test stack that works together. Here’s a focused test using MockMvc.
package com.sayphim.demo.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class MovieRatingControllerTest {
@Autowired MockMvc mockMvc;
@Autowired ObjectMapper objectMapper;
@Test
void shouldRejectScoreOutOfRange() throws Exception {
RateMovieRequest req = new RateMovieRequest("mv-001", 99);
mockMvc.perform(post("/api/movies/rate")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error").value("Validation failed"))
.andExpect(jsonPath("$.fields.score").value("score must be between 0 and 10"));
}
}
No custom surefire gymnastics, no test engine hunting, no “why is JUnit not discovering tests on CI.” The starter ecosystem is designed to reduce that kind of friction.
4. The Maven Archetype Angle: What You Gain, What You Lose, and Why It Matters
Now let’s talk about Maven archetypes honestly. An archetype can generate a project that already contains a controller, a test, a CI file, and an opinionated structure. That feels great. But once the project exists, the archetype stops being part of the story. You cannot say “upgrade my archetype baseline” and have existing projects update automatically.
That’s the key limitation: archetypes optimize creation time, but they don’t optimize evolution time. In a small solo repo, that might be fine. In a company with dozens of services, it’s often the start of drift.
If you’re using an archetype to create Spring Boot services, you’re usually using it to enforce structure, naming, and extra files around the Spring project. But for Spring itself, starters already solve the most painful part: dependency coherence and feature composition. So archetypes become more valuable for organization-level scaffolding than framework-level configuration.
4.1 Where Archetypes Still Make Sense (Even If You Love Starters)
I still like archetypes in a few scenarios. If I need a strict multi-module layout that Boot Initializr won’t generate the way we want, an archetype is a fair choice. If I’m generating a non-Spring Java project—CLI tools, libraries, integration test harnesses, legacy frameworks—archetypes are a solid starter pistol. If compliance requires that every repo ships with a standard set of files (LICENSE, CODEOWNERS, security.md, a pinned CI workflow), an archetype can create that baseline instantly.
But I don’t pretend archetypes keep that baseline healthy over time. That requires either disciplined governance or a different mechanism.
4.2 The “Best of Both” Strategy: Archetype for Repo Shape, Starter for Behavior
A practical approach I’ve seen work well is this: use an archetype to generate repo structure and org-specific files, then use Spring Boot starters—especially internal starters—to enforce consistent runtime behavior.
That way, your archetype sets up “where things live,” and your starters set up “how things behave.” When you want to improve behavior, you update starter versions, not copy-paste new configurations into every repo.
5. The Power Move: Creating a Custom Spring Boot Starter for Your Team
This is where the starter approach becomes more than convenience. A custom starter is basically your internal platform packaged as a dependency. If your organization keeps repeating the same setup across services—logging format, correlation IDs, standardized error responses, security headers, actuator exposure, OpenTelemetry wiring—then copying it into every repo is the slowest possible way to stay consistent.
Instead, you can build a starter like company-platform-starter that auto-configures those defaults when added to a project. Your service POM stays clean, and standards become upgradeable.
5.1 Minimal Custom Starter Example: Auto-Register a Filter and Standardize Headers
I’ll show a small example: a starter that adds a request correlation ID if missing, and returns it in response headers. This is something many teams implement repeatedly, slightly differently, then suffer later when tracing becomes inconsistent.
First, a filter.
package com.sayphim.platform;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
public class CorrelationIdFilter extends OncePerRequestFilter {
public static final String HEADER = "X-Correlation-Id";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String correlationId = request.getHeader(HEADER);
if (correlationId == null || correlationId.isBlank()) {
correlationId = UUID.randomUUID().toString();
}
response.setHeader(HEADER, correlationId);
filterChain.doFilter(request, response);
}
}
Now the auto-configuration that registers it only if the app is a web app.
package com.sayphim.platform;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnWebApplication
public class PlatformAutoConfiguration {
@Bean
public FilterRegistrationBean<correlationidfilter> correlationIdFilter() {
FilterRegistrationBean<correlationidfilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new CorrelationIdFilter());
bean.setOrder(0);
return bean;
}
}
Finally, your starter module includes this auto-config class in the right metadata so Spring Boot discovers it. In Boot 3, you’d add:
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.sayphim.platform.PlatformAutoConfiguration
Now any Spring Boot service can simply add your internal starter dependency, and instantly the correlation ID behavior is consistent across the fleet. That’s the kind of “setup simplification” archetypes cannot keep delivering after day one.
If I’m building a Spring Boot app, I treat starters as the default foundation. They reduce dependency chaos, accelerate feature additions, and make upgrades survivable. If I need strict repo structure, org policy files, or a complex multi-module baseline, I’ll consider an archetype—but I’ll still rely on starters and internal starters for runtime consistency.
If you force archetypes to solve what starters solve, you get duplicated configuration and version drift. If you force starters to solve what archetypes solve, you end up trying to encode repo layout and governance into dependencies, which is awkward. The clean approach is to let each do what it’s good at, and use custom starters as your long-term standardization engine.
And honestly, if your team is copying the same “common config” into five repos, you’re already one step away from building a starter. The sixth repo is where the copy-paste starts charging rent.
7. Wrapping It Up: The Setup That Stays Simple Is the Setup That Can Evolve
Spring Boot Starters simplify setup by turning capabilities into dependencies with coordinated versions and auto-configuration. Maven Archetypes simplify setup by generating a project skeleton quickly. The difference is that starters keep helping as your project evolves, while archetypes mostly help only at the beginning. When you care about long-term consistency—especially across many services—custom starters become the most scalable way to encode standards without turning your organization into a template maintenance team.
If you want, comment below with what you’re currently using—starters only, archetype only, or a mix—and tell me the biggest setup pain you’re trying to eliminate.
Read more at : Spring Boot Starters vs Maven Archetypes Simplify Project Setup (And When Each One Wins)