From 98e2ab5c544ca236ce1c2ddbe54394b483a65147 Mon Sep 17 00:00:00 2001 From: Ivo Spijkerman Date: Thu, 23 Oct 2025 21:53:47 +0200 Subject: [PATCH] docs: add AI generated, human curated javadocs and README.md --- README.md | 151 ++++++++++++++++- .../domain/person/PersonService.java | 157 +++++++++++++++++- 2 files changed, 296 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b517aaa..e72dae6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,148 @@ -# Three Kid Family application -TODO intro and purpose +# Three Kid Family Application -## Development instructions +A Spring Boot application that manages person records and identifies families matching specific criteria: couples with exactly three children where at least one child is under 18 years old. -Requirements +## Overview -Considerations +This service processes person records in real-time, maintaining bidirectional relationship integrity between partners, parents, and children. After each record is received, it immediately determines if any stored person satisfies the "valid parent" pattern and returns all matching individuals. -Limitations +### Valid Parent Criteria -## Run instructions -Requirements \ No newline at end of file +A person is considered a valid parent when they meet the following conditions: + +1. **Has a partner** - partnerId is not null +2. **Has exactly 3 children** - childIds contains exactly 3 IDs +3. **Shared parenthood** - All 3 children list both this person AND their partner as parents +4. **At least one minor** - At least one child has a birthDate making them 18 years old or younger + +## Features + +- **Bidirectional relationship integrity** - Relationships are automatically synchronized in both directions (if A lists B as a child, B automatically lists A as a parent) +- **Real-time validation** - Pattern matching occurs immediately after each upsert +- **In-memory caching** - Valid parents are cached for optimal performance +- **Concurrent request handling** - Method-level locking prevents race conditions +- **PostgreSQL persistence** - All data is stored in a PostgreSQL database + +## Architecture + +### Key Components + +- **PersonController** - REST API endpoint handler +- **PersonService** - Core business logic with relationship management and caching +- **PersonRepository** - JPA repository for database operations +- **Person** - Domain entity representing an individual with relationships + +### Design Decisions + +**Method-level locking over optimistic locking**: The service uses `@Locked` annotations rather than database-level optimistic locking to prevent transaction conflicts when relationship graphs overlap between concurrent requests. This provides better performance for workloads with high relationship overlap. + +**In-memory valid parent cache**: Rather than querying the database for valid parents after each upsert, the service maintains a `Set` cache that is incrementally updated. This reduces database load. + +**Related persons map pattern**: The `retrieveRelatedOf()` method fetches all partner/parent/child entities in a single query and passes them through the call chain, avoiding N+1 queries in most scenarios. + +## API Reference + +### POST /api/v1/people + +Upsert a person record and receive all currently matching valid parents. + +**Request Body:** +```json +{ + "id": 1, + "name": "John Doe", + "birthDate": "1992-03-14", + "partner": {"id": 2}, + "parent1": {"id": 3}, + "parent2": {"id": 4}, + "children": [ + {"id": 5}, + {"id": 6}, + {"id": 7} + ] +} +``` + +**Response:** + +- **200 OK** - One or more valid parents exist. Returns array of all matching persons. +- **204 No Content** - No valid parents currently match the criteria. + +**Note:** The specification requests HTTP 444, but since this is a non-standard nginx status code, the implementation returns 204 No Content instead. + +### DELETE /api/v1/people (Phase 2 - Not Implemented) + +Delete person records and blacklist their IDs permanently. + +**Request Body:** +```json +[1, 2, 3] +``` + +## Development Instructions + +### Requirements + +- **Java 21+** (OpenJDK or Eclipse Temurin recommended) +- **Maven 3.9+** +- **Docker & Docker Compose** (for containerized deployment) +- **PostgreSQL 15** (or use Docker Compose setup) + +### Local Development Setup + +1. **Clone the repository** + ```bash + git clone + cd threekidfamily + ``` + +2. **Configure database connection** (if not using Docker) + + Edit `application.properties` or set environment variables: + ```properties + SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/mydb + SPRING_DATASOURCE_USERNAME=myuser + SPRING_DATASOURCE_PASSWORD=mypassword + ``` + +3. **Build the application** + ```bash + mvn clean package + ``` + +4. **Run tests** + ```bash + mvn test + ``` + +5. **Run locally** + ```bash + mvn spring-boot:run + ``` + +The application will be available at `http://localhost:8080` + +### Docker Deployment + +The easiest way to run the application with its PostgreSQL database: + +```bash +docker-compose up --build +``` + +The application will be available at `http://localhost:8080` + +## Performance Considerations + +### Optimizations + +- **Caching strategy**: Valid parents are cached in-memory and incrementally updated +- **Batch operations**: Related persons are fetched in bulk to minimize database queries +- **Defensive copies**: Returned collection is deep-copied to prevent external mutations + +## Known Limitations + +1. **Phase 2 not implemented**: DELETE endpoint and blacklisting functionality is not yet available +2. **HTTP 444 substitution**: Returns 204 instead of the specified HTTP 444 status code +3. **MAX_RELATED_ASSUMPTION**: The service assumes no single person has more than 1000 related individuals (partners, parents, children). This prevents potential memory issues. +4. **Full table scan on startup**: `populateCache()` loads all persons into memory on startup. For very large datasets, consider implementing pagination or streaming. diff --git a/src/main/java/com/spijkerman/ivo/threekidfamily/domain/person/PersonService.java b/src/main/java/com/spijkerman/ivo/threekidfamily/domain/person/PersonService.java index f9629e9..54729ba 100644 --- a/src/main/java/com/spijkerman/ivo/threekidfamily/domain/person/PersonService.java +++ b/src/main/java/com/spijkerman/ivo/threekidfamily/domain/person/PersonService.java @@ -14,6 +14,30 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; +/** + * Service for managing person entities and their family relationships. + * + *

This service maintains bidirectional consistency for two types of relationships: + *

    + *
  • Partner relationships: One-to-one, symmetric (if A partners B, then B partners A)
  • + *
  • Parent-child relationships: Many-to-many, symmetric (if A is parent of C, then C has parent A)
  • + *
+ * + *

The service maintains an in-memory cache of all "valid parents" - persons who meet specific + * family criteria (partner + exactly 3 children + at least one child under 18). This cache is + * updated transactionally with each upsert operation to ensure consistency. + * + *

Concurrency Model: Uses method-level locking ({@code @Locked}) rather than + * database-level optimistic locking. This prevents transaction conflicts and retries when + * relationship graphs overlap between concurrent requests, resulting in better performance + * for workloads with high relationship overlap. + * + * @implNote The {@code related} Map is passed through most private methods rather than being + * a field or repeatedly fetched. This design choice optimizes performance. + * + * @see Person + * @see PersonRepository + */ @Service @RequiredArgsConstructor public class PersonService { @@ -24,6 +48,16 @@ public class PersonService { // Cache all the valid parents, so we do not have to recalculate all on each upsert. private final Set parentsInValidFamiliesCache = new HashSet<>(); + /** + * Initializes the valid parent cache by loading all persons from the database + * and filtering those who meet the validity criteria (have a partner, exactly 3 children + * with at least one under 18, and bidirectional parent-child relationships). + * + *

This method is called automatically after dependency injection completes. + * During execution, the method holds a lock to prevent concurrent modifications. + * + * @throws AssertionError if the number of related persons exceeds {@link #MAX_RELATED_ASSUMPTION} + */ @Locked @PostConstruct private void populateCache() { @@ -38,9 +72,41 @@ public class PersonService { } /** - * Upserts a person to storage, overwriting any existing or conflicting data. + * Upserts a person to storage, maintaining bidirectional relationship consistency, + * and returns all currently valid parents. * - * @param newPerson The Person data to be persisted + *

This method performs the following operations atomically: + *

    + *
  1. Retrieves the existing person (if any) and all related persons
  2. + *
  3. Updates bidirectional relationships for partner, parents, and children
  4. + *
  5. Persists all modified persons to the database
  6. + *
  7. Updates the valid parent cache by adding newly valid parents and removing invalidated ones
  8. + *
  9. Returns a defensive deep copy of all currently valid parents
  10. + *
+ * + *

Relationship Rules: + *

    + *
  • Setting a partner automatically updates the partner's partnerId to reference this person
  • + *
  • If the new partner already has a different partner, that relationship is broken first
  • + *
  • Adding a parent automatically adds this person to the parent's childIds
  • + *
  • Adding a child automatically adds this person to the child's parentIds
  • + *
+ * + *

Validity Criteria: A person is considered a valid parent if they: + *

    + *
  • Have a partner (partnerId is not null)
  • + *
  • Have exactly 3 children
  • + *
  • All 3 children list both this person and their partner as parents
  • + *
  • At least one child is under 18 years old (or exactly 18)
  • + *
+ * + *

Thread Safety: This method is synchronized via {@code @Locked} to prevent + * concurrent modifications to overlapping relationship graphs. + * + * @param newPerson The person data to be persisted. Must have a non-null id. + * @return A sorted, immutable collection of all persons who are currently valid parents. + * Each Person is a deep copy, safe from concurrent modifications. + * @throws AssertionError if the number of related persons exceeds {@link #MAX_RELATED_ASSUMPTION} */ @Locked @Transactional @@ -91,6 +157,19 @@ public class PersonService { ); } + /** + * Retrieves all persons who are directly related to the given person, including + * the person's partner, parents, and children. + * + *

For any referenced IDs that do not exist in the database, this method creates + * placeholder Person objects with only the ID set. This ensures that relationship + * updates can proceed even when referencing not-yet-created persons. + * + * @param people The set of persons whose related persons should be retrieved + * @return An unmodifiable map from person ID to Person object. Includes placeholder + * objects for any referenced but non-existent persons. + * @throws AssertionError if the total number of related IDs exceeds {@link #MAX_RELATED_ASSUMPTION} + */ private Map retrieveRelatedOf(Set people) { val ids = new HashSet(); for (val person : people) { @@ -128,6 +207,27 @@ public class PersonService { return result; } + /** + * Updates partner relationships bidirectionally, ensuring consistency. + * + *

This method handles the following scenarios: + *

    + *
  • Setting a new partner: Sets the new partner's partnerId to selfId. + * If the new partner already has a different partner, that relationship is broken first.
  • + *
  • Removing an old partner: Sets the old partner's partnerId to null.
  • + *
  • Changing partners: Combines both operations above.
  • + *
  • No change: Returns an empty set if newPartnerId equals oldPartnerId.
  • + *
+ * + *

Note: May trigger an N+1 query if the new partner's existing partner + * is not in the related map. This is rare and acceptable for typical usage patterns. + * + * @param selfId The ID of the person whose partner is being set + * @param related Map of all related persons, used to look up partner objects + * @param newPartnerId The ID of the new partner, or null to remove partner + * @param oldPartnerId The ID of the previous partner, or null if none existed + * @return A set of all Person objects that were modified (partners whose partnerId changed) + */ private Set setPartner( int selfId, Map related, @@ -167,6 +267,18 @@ public class PersonService { : Optional.of(relatedPartner); } + /** + * Updates parent-child relationships bidirectionally by modifying the parent's childIds. + * + *

For each parent being removed, removes the childId from their childIds set. + * For each parent being added, adds the childId to their childIds set. + * + * @param childId The ID of the child whose parents are being updated + * @param related Map of all related persons, used to look up parent objects + * @param newParentIds The new set of parent IDs (typically 0, 1, or 2 parents) + * @param oldParentIds The previous set of parent IDs + * @return An unmodifiable set of all Person objects that were modified (parents whose childIds changed) + */ private Set setParents( int childId, Map related, @@ -190,6 +302,18 @@ public class PersonService { return Collections.unmodifiableSet(modifiedPersons); } + /** + * Updates parent-child relationships bidirectionally by modifying the child's parentIds. + * + *

For each child being removed, removes the parentId from their parentIds set. + * For each child being added, adds the parentId to their parentIds set. + * + * @param parentId The ID of the parent whose children are being updated + * @param related Map of all related persons, used to look up child objects + * @param newChildIds The new set of child IDs + * @param oldChildIds The previous set of child IDs + * @return An unmodifiable set of all Person objects that were modified (children whose parentIds changed) + */ private Set setChildren( int parentId, Map related, @@ -220,6 +344,22 @@ public class PersonService { .collect(Collectors.toSet()); } + /** + * Determines whether a person meets the criteria to be considered a valid parent. + * + *

Validity Criteria: + *

    + *
  1. Has a partner (partnerId is not null)
  2. + *
  3. Has exactly 3 children
  4. + *
  5. All 3 children list both this person and their partner as parents
  6. + *
  7. At least one child has a birthDate that is 18 years ago or more recent
  8. + *
+ * + * @param person The person to validate + * @param related Map of related persons, used to look up partner and children. + * Must contain all persons referenced by person's partnerId and childIds. + * @return true if the person meets all validity criteria, false otherwise + */ private boolean isValid(Person person, Map related) { // 1. Has a partner if (person.partnerId() == null) { @@ -246,9 +386,18 @@ public class PersonService { } /** - * Deletes all data related to the specified user, and blacklists that ID from later use. + * Deletes all data related to the specified user and blacklists that ID from later use. * - * @param id The ID of the person to be deleted and blacklisted. + *

Not yet implemented. When implemented, this method should: + *

    + *
  • Remove all bidirectional relationships (partner, parents, children)
  • + *
  • Delete the person from the database
  • + *
  • Remove the person from the valid parent cache
  • + *
  • Prevent the ID from being reused (blacklist mechanism TBD)
  • + *
+ * + * @param id The ID of the person to be deleted and blacklisted + * @throws UnsupportedOperationException always, until this method is implemented */ @Locked public void deletePersonById(int id) {