Developer: Justin Guida
Date: December 11, 2025
Status: Accepted
This file explains, in my own words, the main pieces of this project:
the Quote / Value JSON model, the QuoteController REST client,
and the Spring Boot application setup (entry point, config, tests).
| File | Role | Notes |
|---|---|---|
Quote.java |
Record for JSON response | Shared JSON shape with service |
Value.java |
Nested record (id, quote text) |
Shared JSON shape with service |
QuoteController.java |
REST client controller, fetches quote from API | Uses RestClient, error handling |
ConsumingRestApplication.java |
Main Spring Boot application entry point | Boots the app on port 8081 |
application.properties |
Configuration | Port + quote-service base URL |
ConsumingRestApplicationTests.java |
Tests | Spring Boot test scaffold |
QuoteControllerTest.java |
Endpoint tests with mocked backend | MockRestServiceServer |
Shared JSON shape between quote-service and consuming client.
@JsonIgnoreProperties(ignoreUnknown = true)
public record Quote(String type, Value value) { }@JsonIgnoreProperties(ignoreUnknown = true) tells Jackson to ignore any
JSON properties that do not have a corresponding field in the Quote record.
This is useful if the JSON response contains extra data that I do not care
about.
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 JSON response from
the quote-service into Java, and then back into JSON when my /quote
endpoint returns 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 (e.g., inside QuoteController.java):
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 (e.g., inside QuoteController.java):
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.)
@JsonIgnoreProperties(ignoreUnknown = true)
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.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
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.
@JsonIgnoreProperties(ignoreUnknown = true)
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 consumes the quote-service API and exposes /quote endpoint.
Key annotations:
@RestController- makes this a REST endpoint that returns JSON@GetMapping("/quote")- maps GET requests to the handler method@Value("${quote.service.base-url}")- injects property from application.properties
RestClient usage (Spring Boot 3.2+):
this.restClient = builder.baseUrl(baseUrl).build();
// In the handler method:
return restClient
.get().uri("/api/random")
.retrieve()
.body(Quote.class);The fluent API:
.get()- HTTP GET request.uri("/api/random")- appended to base URL.retrieve()- executes the request.body(Quote.class)- deserializes JSON to Quote record
Error handling:
try {
return restClient.get()...;
} catch (RestClientException e) {
log.error("Failed to fetch quote", e);
return new Quote("error", new Value(-1L, "Quote service unavailable"));
}Returns a fallback response instead of crashing when quote-service is down. See ADR-0003 for rationale.
Standard Spring Boot entry point. Nothing special here.
@SpringBootApplication
public class ConsumingRestApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumingRestApplication.class, args);
}
}What @SpringBootApplication does:
@Configuration- marks class as bean definition source@EnableAutoConfiguration- enables Spring Boot auto-config (including RestClient.Builder bean)@ComponentScan- scans current package for@Component,@RestController, etc.
Configuration for this module.
server.port=8081
quote.service.base-url=http://localhost:8080| Property | Purpose |
|---|---|
server.port=8081 |
Avoids conflict with quote-service (which uses 8080) |
quote.service.base-url |
Externalized backend URL for RestClient |
Why externalize the URL?
- Easy to change without code modifications
- Different values for dev/test/prod environments
- Can override via command line:
-Dquote.service.base-url=http://prod:8080 - See ADR-0004 for rationale
Tests the QuoteController using @SpringBootTest with @AutoConfigureMockMvc and @AutoConfigureMockRestServiceServer.
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureMockRestServiceServer
class QuoteControllerTest {
@Autowired MockMvc mockMvc;
@Autowired MockRestServiceServer server;
}Key testing patterns:
MockRestServiceServer- mocks HTTP responses without real network callsserver.expect()- sets up expected requests and canned responsesMockMvc- tests HTTP endpoints without starting a real server- Tests both happy path (backend returns quote) and error path (backend unavailable)