Developer: Justin Guida Date: December 11, 2025 Status: Draft
This file explains, in my own words, the main pieces of this project:
the Quote / Value JSON model, the QuoteController REST endpoints,
and the Spring Boot application setup (entry point, config, tests).
| File | Role | Notes |
|---|---|---|
Quote.java |
Record for JSON response | Shared JSON shape with consumer |
Value.java |
Nested record (id, quote text) |
Shared JSON shape with consumer |
QuoteController.java |
REST controller, serves quotes | Streams, @PathVariable, ThreadLocalRandom |
QuoteServiceApplication.java |
Main Spring Boot application entry point | Boots the app on port 8080 |
application.properties |
Configuration | Default port 8080 |
QuoteControllerTest.java |
Tests for all 3 endpoints | @WebMvcTest, MockMvc |
Shared JSON shape between quote-service and consuming client.
public record Quote(String type, Value value) { }This file defines a Quote record. Using a record lets me represent the
response with less boilerplate than a normal Java class.
Quote has two fields:
type- aStringrepresenting the type of response (for example,"success")value- aValueobject that holds theidand the actual quote text
Spring, through Jackson, uses this record to map the Java object into JSON when the endpoints return it.
{
"type": "success",
"value" : { "id": 1, "quote": "..."}
}Records replace all that boilerplate - no manual constructors, getters,
toString(), equals(), or hashCode(). One line does it all.
There are many ways you can code this, for example using final vs
non-final fields, or using getters/setters vs direct field access in
simple examples. This is why using records is nice - it reduces the
complexity of defining data-holding types, and it simply works with
Jackson for JSON mapping.
This is how you can write a class by hand - pass all data up
front with new Quote("success", value).
public class Quote {
private String type;
private Value value;
// All-args constructor
public Quote(String type, Value value) {
this.type = type;
this.value = value;
}
// Getters
public String getType() { return type; }
public Value getValue() { return value; }
// Optional setters (only if you want mutability)
public void setType(String type) { this.type = type; }
public void setValue(Value value) { this.value = value; }
}Unlike the no-arg + setters pattern that Jackson uses by default
(new Quote() then setType(...) / setValue(...)), this style
expects all data up front in the constructor call.
This is the pattern Jackson uses by default for JSON deserialization. Jackson calls the no-arg constructor, then uses setters to fill fields.
public class Quote {
private String type;
private Value value;
// No-arg constructor (explicit or omitted; both are fine)
public Quote() { }
// Getters
public String getType() { return type; }
public Value getValue() { return value; }
// Setters - Jackson calls these after construction
public void setType(String type) { this.type = type; }
public void setValue(Value value) { this.value = value; }
}| Component | What it does | Code |
|---|---|---|
| Fields | Data storage inside the object | private String type;private Value value; |
| Constructor | How the object is created | public Quote() { } |
| Setters | How we change the fields | setType(...), setValue(...) |
| Getters | How we read the fields | getType(), getValue() |
Summary:
| Component | Purpose |
|---|---|
| Fields | Data storage inside the object |
| Constructor | How the object is created |
| Setters | How we change the fields |
| Getters | How we read the fields |
When you call this:
Quote q = new Quote();This happens:
- JVM allocates a new Quote object.
- The fields
typeandvalueare created inside that object and get default values:type= nullvalue= null
- The constructor
public Quote()runs (it does nothing extra here).
After new Quote():
- The object exists.
- The fields exist, but they are still null.
Key Point: Nothing has called setType or setValue yet.
When you call this:
q.setType("success");
q.setValue(new Value(1L, "some quote"));This happens:
| Method | What runs | Result |
|---|---|---|
setType("success") |
this.type = type; |
type changes from null to "success" |
setValue(...) |
this.value = value; |
value changes from null to Value instance |
Summary:
- The fields are created at construction (
new Quote()). - They are filled/changed when setters are called.
When Jackson deserializes JSON into Quote, it does:
Quote q = new Quote();(calls the no-arg constructor)q.setType(jsonTypeValue);q.setValue(jsonValueObject);
Key Point: Same exact steps as above, just done automatically by Jackson.
What happens to the field type:
| When | What happens |
|---|---|
At construction (new Quote()) |
type is created with default null |
Later, in setType(...) |
type is assigned a real value |
That means the field changes after the object is constructed.
If you wrote:
private final String type;
private final Value value;Java rules say:
- A final field must be assigned exactly once, in a constructor or at declaration.
- You cannot assign to it later in a setter.
So with final:
public Quote() { }is illegal unless you set bothtypeandvalueinside it.setTypeandsetValuewould not be allowed to dothis.type = ...orthis.value = ...because that would change a final field.
This is why:
"This pattern needs non-final fields because we change them after construction."
We change them here:
public void setType(String type) {
this.type = type; // this is a change after construction
}If the field was final, this line would not be allowed.
| What | When |
|---|---|
| Object created | new Quote() runs |
| Fields exist | At creation, but are null |
| Fields filled | When setters are called |
| Why not final | Setters change fields after construction |
There are two common ways to map JSON into a Quote class:
- Non-final fields with a no-arg constructor and setters (mutable)
- Final fields with a constructor only (immutable), which is what records give you
(This is the same pattern as "Example: No-arg constructor + setters (Jackson default)" above. Repeated here to compare with the final-field version.)
public class Quote {
private String type;
private Value value;
// 1) Jackson calls this no-arg constructor.
// In Java, if you do not write any constructor, the compiler
// will create this empty no-arg constructor for you. I am
// writing it explicitly here to show the "new Quote()" step.
public Quote() { }
// 2) Then Jackson calls these setters to fill the fields
public void setType(String type) {
this.type = type;
}
public void setValue(Value value) {
this.value = value;
}
// Getters so we can read the values
public String getType() {
return type;
}
public Value getValue() {
return value;
}
}What happens:
- Jackson does
new Quote()(fields exist but are null). - Jackson calls
setType(jsonType)andsetValue(jsonValue)to fill the fields.
Key Point: Fields cannot be final here, because setters need to change them after construction.
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Quote {
private final String type;
private final Value value;
// Jackson uses THIS constructor directly
@JsonCreator
public Quote(
@JsonProperty("type") String type,
@JsonProperty("value") Value value) {
this.type = type;
this.value = value;
}
// Only getters, no setters (object is immutable)
public String getType() {
return type;
}
public Value getValue() {
return value;
}
}What happens:
- Jackson sees
@JsonCreatoron the constructor. - It reads JSON fields
"type"and"value". - It calls
new Quote(typeFromJson, valueFromJson). - The fields are set once in the constructor and never change, so they can be final.
Key Point: Here we do not use setters at all. All data comes in through the constructor once.
A Java record is basically the second pattern, but the compiler writes the constructor and getters for me.
public record Quote(String type, Value value) { }The compiler generates something very close to:
public final class Quote {
private final String type;
private final Value value;
public Quote(String type, Value value) {
this.type = type;
this.value = value;
}
public String type() { return type; }
public Value value() { return value; }
// plus equals, hashCode, toString...
}Summary of the three patterns:
| Pattern | Description |
|---|---|
| Non-final + setters | Easy, mutable, uses no-arg constructor + setters |
| Final + constructor | Immutable, fields set once in the constructor |
| Record | Final + constructor pattern with less boilerplate |
This is why using a record Quote(String type, Value value) is a clean fit
for this project. You get the immutable "final field + constructor" style
without writing all the boilerplate by hand.
Shared JSON shape between quote-service and consuming client.
Represents the nested "value" part of the JSON: an id and the quote
text itself.
public record Value(Long id, String quote) { }REST controller that serves quotes as JSON. See also concepts/quote-controller.md for detailed explanation.
Key annotations:
@RestController- combines@Controller+@ResponseBody, returns JSON directly@RequestMapping("/api")- sets base path for all endpoints@GetMapping- maps HTTP GET to methods@PathVariable- extracts path segments (e.g.,/api/5->id=5)
Data structure:
private static final List<Value> QUOTES = List.of(...);List.of()creates an immutable list (Java 9+)- Static field means quotes are shared across all requests
- Immutability is thread-safe for concurrent access
Endpoints:
| Path | Method | Returns |
|---|---|---|
/api/ |
all() |
All quotes as List |
/api/random |
random() |
Single random quote |
/api/{id} |
byId() |
Quote by ID or HTTP 404 |
Thread-safe random selection:
Value value = QUOTES.get(ThreadLocalRandom.current().nextInt(QUOTES.size()));ThreadLocalRandomavoids contention in concurrent requests- Each thread gets its own Random instance
- See ADR-0003 for rationale
Standard Spring Boot entry point. Nothing special here - just boots the application.
@SpringBootApplication
public class QuoteServiceApplication {
public static void main(String[] args) {
SpringApplication.run(QuoteServiceApplication.class, args);
}
}What @SpringBootApplication does:
@Configuration- marks class as bean definition source@EnableAutoConfiguration- enables Spring Boot auto-config@ComponentScan- scans current package for@Component,@RestController, etc.
Configuration for this module.
# Server runs on default port 8080
# No explicit configuration needed for this module| Property | Purpose |
|---|---|
server.port |
Default 8080; consuming-rest uses 8081 to avoid conflict |
Port configuration:
- Default:
8080 - Override with:
server.port=8081in properties - Or via command line:
./mvnw spring-boot:run -Dspring-boot.run.arguments=--server.port=8081
Tests the QuoteController using @WebMvcTest to test the web layer in isolation.
@WebMvcTest(QuoteController.class)
class QuoteControllerTest {
@Autowired MockMvc mockMvc;
}Key testing patterns:
@WebMvcTest- loads only web layer, not full application context (fast)MockMvc- simulates HTTP requests without starting a real server- Tests all 3 endpoints:
/api/,/api/random,/api/{id}
Tests:
| Test | What it checks |
|---|---|
getAllQuotes_returnsListOf10Quotes |
/api/ returns 10 quotes with type="success" |
getRandomQuote_returnsSuccessWithValidId |
/api/random returns a valid quote |
getQuoteById_withValidId_returnsQuote |
/api/5 returns the correct quote |
getQuoteById_withInvalidId_returns404 |
/api/999 returns HTTP 404 Not Found |