Transform an Existing GET Response to a New Response Contract (Spring Boot)
February 16, 2026
This question came up in one of the interviews I had recently. Felt like a good idea to write about it.
The question given was this :
Problem
You are working on a Spring Boot backend that already exposes an endpoint GET /customers
This endpoint is already implemented and returns customer data in the following format (current contract)
{
"customers": [
{
"id": 101,
"first_name": "Asha",
"last_name": "Rao",
"email": "asha.rao@gmail.com",
"phone": "512-111-2222",
"address": {
"city": "Austin",
"state": "TX",
"zip": "78735"
},
"account": {
"status": "ACTIVE",
"created_at": "2024-11-05T14:20:10Z"
},
"spend": {
"lifetime": 1240.75
}
}
]
}The task is that without changing how customer data is fetched (i.e., do not modify repository/external call logic), update the API behavior so that the same endpoint returns data in the following format (new required contract):
{
"results": [
{
"customerId": 101,
"fullName": "Asha Rao",
"contact": {
"email": "asha.rao@gmail.com",
"phone": "512-111-2222"
},
"location": "Austin, TX 78735",
"isActive": true,
"lifetimeSpend": 1240.75,
"accountCreatedDate": "2024-11-05"
}
],
"count": 1
}The constraint are :
- thats given is that the api call will always return
<=100 customers, and if 0 customers are found, it will return this{ "results": [], "count": 0 } - Fields may be missing or empty; your code should not throw
NullPointerException
Solution
Before you look at the solution, would highly recommend you to try solving this question yourself, and then compare your solution with the one i have provided.
Lets solve this question step by step..
Assume this is the current structure of the controller level endpoint :
@RestController
@RequestMapping("/customers")
public class CustomerController {
private final CustomerService customerService;
@Autowired
public CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
@GetMapping
public ResponseEntity<CustomersResponse> getCustomers() {
CustomersResponse response = customerService.getCustomers();
return ResponseEntity.ok(response);
}
}
Now i have to transform this into the new API contract as asked in the question.. So how can we do this ??
i could consider the input dto as the output from the service layer..., and then i need to take this input dto and transform it to a output dto as asked in the question..
To achieve this, we can create a separate mapping class responsible for handling the transformation logic. This class will contain a method that converts the input DTO into the output DTO.
Then, at the controller level, we will call the service to retrieve the input DTO, pass it to the mapper, and return the transformed output DTO as the final API response.
So lets create the output dto class first :
public record NewCustomerDTO(
Long customerId,
String fullName,
Contact contact,
String location,
boolean isActive,
double lifetimeSpend,
String accountCreatedDate
) {
public record Contact(String email, String phone) {}
}Now lets create the class that does the mapping :
In this class we will have to transform and map for every customer and their fields, so will have to get from the input dto and then set the values to the output dto appropriately. Created that mapping function here :
remember, if we are using a records, we cant use setters, so we have to use constructor to set the values.
public NewCustomerDTO toNewContract(Customer customer) {
String fullName = customer.getFirstName() + " " + customer.getLastName();
NewCustomerDTO.Contact contact =
new NewCustomerDTO.Contact(customer.getEmail(), customer.getPhone());
String location = customer.getAddress().getCity() + ", "
+ customer.getAddress().getState() + " "
+ customer.getAddress().getZip();
boolean isActive = "ACTIVE".equals(customer.getAccount().getStatus());
double lifetimeSpend = customer.getSpend().getLifetime();
String accountCreatedDate = customer.getAccount().getCreatedAt().toLocalDate().toString();
return new NewCustomerDTO(
customer.getId(),
fullName,
contact,
location,
isActive,
lifetimeSpend,
accountCreatedDate
);
}Now we will have to put this inside a mapper class, and then iterate over all customers and then call this function. lets stitch this together :
@Component
class CustomMapper {
public List<NewCustomerDTO> convertAllCustomers(List<Customer> customers) {
List<NewCustomerDTO> convertedCustomers = new ArrayList<>();
if (customers == null) {
return convertedCustomers;
}
for (Customer customer : customers) {
NewCustomerDTO dto = toNewContract(customer);
if (dto != null) {
convertedCustomers.add(dto);
}
}
return convertedCustomers;
}
public NewCustomerDTO toNewContract(Customer customer) {
if (customer == null) return null;
String fullName = customer.getFirstName() + " " + customer.getLastName();
NewCustomerDTO.Contact contact =
new NewCustomerDTO.Contact(customer.getEmail(), customer.getPhone());
String location = customer.getAddress().getCity() + ", "
+ customer.getAddress().getState() + " "
+ customer.getAddress().getZip();
boolean isActive = "ACTIVE".equals(customer.getAccount().getStatus());
double lifetimeSpend = customer.getSpend().getLifetime();
String accountCreatedDate = customer.getAccount().getCreatedAt().toLocalDate().toString();
return new NewCustomerDTO(
customer.getId(),
fullName,
contact,
location,
isActive,
lifetimeSpend,
accountCreatedDate
);
}
}
Let's update the controller layer (Assume the service gives us List<Customer> already):
@RestController
@RequestMapping("/customers")
public class CustomerController {
@Autowired
private CustomMapper customMapper;
@Autowired
private CustomerService customerService;
@GetMapping
public ResponseEntity<List<NewCustomerDTO>> getAllCustomers() {
List<Customer> customers = customerService.getCustomers();
List<NewCustomerDTO> newCustomers = customMapper.convertAllCustomers(customers);
return ResponseEntity.ok(newCustomers);
}
}Doneee!! We did it, but a few more nuances to consider :
We did not hanfle the case of safely returning incase of nulls, because now all of them return a NPE..
So lets handle that :
@Component
class CustomMapper {
public List<NewCustomerDTO> convertAllCustomers(List<Customer> customers) {
List<NewCustomerDTO> convertedCustomers = new ArrayList<>();
if (customers == null) return convertedCustomers;
for (Customer customer : customers) {
NewCustomerDTO dto = toNewContract(customer);
if (dto != null) convertedCustomers.add(dto);
}
return convertedCustomers;
}
public NewCustomerDTO toNewContract(Customer customer) {
if (customer == null) return null;
String fullName = (safe(customer.getFirstName()) + " " + safe(customer.getLastName())).trim();
NewCustomerDTO.Contact contact =
new NewCustomerDTO.Contact(safe(customer.getEmail()), safe(customer.getPhone()));
Address address = customer.getAddress();
String location = buildLocation(address);
Account account = customer.getAccount();
boolean isActive = account != null && "ACTIVE".equals(account.getStatus());
Spend spend = customer.getSpend();
double lifetimeSpend = (spend == null || spend.getLifetime() == null) ? 0.0 : spend.getLifetime();
String createdAt = (account == null) ? null : safe(account.getCreatedAt()); // if String
String accountCreatedDate = dateOnly(createdAt);
return new NewCustomerDTO(
customer.getId(),
fullName,
contact,
location,
isActive,
lifetimeSpend,
accountCreatedDate
);
}
private String safe(String s) {
return s == null ? "" : s.trim();
}
private String buildLocation(Address a) {
if (a == null) return "";
String city = safe(a.getCity());
String state = safe(a.getState());
String zip = safe(a.getZip());
String cityState = "";
if (!city.isBlank() && !state.isBlank()) cityState = city + ", " + state;
else if (!city.isBlank()) cityState = city;
else if (!state.isBlank()) cityState = state;
if (cityState.isBlank()) return zip;
return zip.isBlank() ? cityState : (cityState + " " + zip);
}
// ISO string like "2024-11-05T14:20:10Z" -> "2024-11-05"
private String dateOnly(String createdAt) {
if (createdAt == null || createdAt.isBlank()) return "";
int t = createdAt.indexOf('T');
return t > 0 ? createdAt.substring(0, t) : createdAt;
}
}
before we end, there is one more thing to add, and that is the count field in the json response if you remember..
For that we just need to create a wrapper DTO and then change our code in 2 places, so i will replace those 2 places here :
public record CustomersResponseDTO(
List<NewCustomerDTO> results,
int count
) {}
public CustomersResponseDTO convertAllCustomers(List<Customer> customers) {
List<NewCustomerDTO> results = new ArrayList<>();
if (customers == null) {
return new CustomersResponseDTO(results, 0);
}
for (Customer customer : customers) {
NewCustomerDTO dto = toNewContract(customer);
if (dto != null) results.add(dto);
}
return new CustomersResponseDTO(results, results.size());
}
Everything else in the mapper, remains the same.
And in the controller class, wechange this
@RestController
@RequestMapping("/customers")
public class CustomerController {
@Autowired
private CustomMapper customMapper;
@Autowired
private CustomerService customerService;
@GetMapping
public ResponseEntity<CustomersResponseDTO> getAllCustomers() {
List<Customer> customers = customerService.getCustomers();
CustomersResponseDTO response = customMapper.convertAllCustomers(customers);
return ResponseEntity.ok(response);
}
}