Sample Application
The repository ships a runnable demo under
sample/ that exercises
all modules: Mongo and JPA side by side, plain String ids, hashids and
TSID ids, parent-child resources and Testcontainers-based integration tests.
| Domain | Store | ID exposed as | Shows |
|---|---|---|---|
| Company | MongoDB | plain String | flat CRUD |
| Employee | MongoDB | plain String | parent-child under Company |
| Customer | JPA (H2) | ObfuscatedId (hashids) |
id obfuscation |
| Location | JPA (H2) | TSID |
time-sorted ids, read-only resource |
A resource end to end — Company
Section titled “A resource end to end — Company”Entity — persistence only:
@Document(collection = "company")@Data @Builder @NoArgsConstructor @AllArgsConstructorpublic class CompanyEntity implements Serializable { @Id private String id; private String name; private String email; private String url;}Read / Write DTOs — see Concepts for the shape and the reasoning.
Converter — implements EntityReadWriteConverter; fromEntities(List) comes free as a
default method:
@Componentpublic class CompanyConverter implements EntityReadWriteConverter<CompanyEntity, CompanyRead, CompanyWrite> {
@Override public CompanyRead fromEntity(CompanyEntity entity) { if (entity == null) return null; return CompanyRead.builder() .id(entity.getId()).name(entity.getName()) .email(entity.getEmail()).url(entity.getUrl()) .build(); }
@Override public CompanyEntity newEntity(CompanyWrite write) { return CompanyEntity.builder() .name(write.getName()).email(write.getEmail()).url(write.getUrl()) .build(); }
@Override public CompanyEntity updateEntityFromEdit(CompanyWrite write, CompanyEntity entity) { entity.setName(write.getName()); entity.setEmail(write.getEmail()); entity.setUrl(write.getUrl()); return entity; }}Controller — implements the shared CompanyApi interface:
@RestController@RequestMapping(path = "/api")@RequiredArgsConstructorpublic class CompanyController implements CompanyApi {
private final CompanyRepository repository; private final CompanyConverter converter;
@Override public PageableResult<CompanyRead> find(Pageable pageable) { Page<CompanyEntity> page = repository.findAll(pageable); return PageableResult.contentPage(converter.fromEntities(page.getContent()), page); }
@Override public CompanyRead findById(String id) { return converter.fromEntity(getEntity(id)); }
@Override public CompanyRead create(CompanyWrite write) { return converter.fromEntity(repository.save(converter.newEntity(write))); }
@Override public CompanyRead update(String id, CompanyWrite write) { CompanyEntity entity = converter.updateEntityFromEdit(write, getEntity(id)); return converter.fromEntity(repository.save(entity)); }
@Override public void delete(String id) { repository.delete(getEntity(id)); }
protected CompanyEntity getEntity(String id) { return repository.findById(id) .orElseThrow(NotFoundException::new); }}Parent-child resources — Employee
Section titled “Parent-child resources — Employee”Nested resources live under the parent path; the Write payload doesn’t carry the parent id (it comes from the URL), and the Read type embeds the resolved parent:
public interface EmployeeApi {
@GetMapping(path = "/company/{parentId}/employee") PageableResult<EmployeeRead> find(@PathVariable("parentId") String parentId, @ParameterObject Pageable pageable);
@PostMapping(path = "/company/{parentId}/employee") @ResponseStatus(HttpStatus.CREATED) EmployeeRead create(@PathVariable("parentId") String parentId, @RequestBody @NotNull @Valid EmployeeWrite write);}public class EmployeeRead implements Serializable { private String id; private String firstName; private String lastName; private String email; private CompanyRead company; // resolved parent, embedded in the response}The repository scopes every access to the parent — fetching a child through the wrong parent yields a 404:
public interface EmployeeRepository extends MongoRepository<EmployeeEntity, String> { Optional<EmployeeEntity> findFirstByCompanyIdAndId(String companyId, String id); Page<EmployeeEntity> findAllByCompanyId(String companyId, Pageable pageable);}protected EmployeeEntity getEntity(String parentId, String id) { return repository.findFirstByCompanyIdAndId(parentId, id) .orElseThrow(NotFoundException::new);}Integration tests
Section titled “Integration tests”The sample’s tests combine rest-assured (MockMvc) with Testcontainers — Spring Boot’s
@ServiceConnection wires the Mongo container without any manual property plumbing:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)@Testcontainerspublic abstract class BaseIntegrationTest {
@Container @ServiceConnection protected static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:8"));}Patterns worth copying:
- assert the problem details shape on validation errors:
.body("fields.email", not(emptyString())) - deserialize paged responses into
PageableResult<T>via a rest-assuredTypeRefand asserttotalElements/page/totalPages - verify parent-child isolation: the right child id under the wrong parent id must yield 404
Running it
Section titled “Running it”cd sample/sample-servermvn spring-boot:runAn H2 in-memory database covers the JPA entities; a DataInitializer seeds demo data on
first start.