{"id":50811978,"url":"https://github.com/kiquetal/java21-workout","last_synced_at":"2026-06-13T05:33:38.898Z","repository":{"id":349236996,"uuid":"1200501663","full_name":"kiquetal/java21-workout","owner":"kiquetal","description":"Java 21 exercises focusing on advanced persistence with JPA. Explore optimistic and pessimistic locking, transaction management, and database concurrency patterns. A practical workout for mastering robust Java enterprise data access.","archived":false,"fork":false,"pushed_at":"2026-06-13T01:58:35.000Z","size":191,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-13T03:22:15.599Z","etag":null,"topics":["concurrency","database","exercises","hibernate","java","java21","jpa","learning","locking","quarkus","workout"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kiquetal.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-03T13:43:23.000Z","updated_at":"2026-06-13T01:58:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kiquetal/java21-workout","commit_stats":null,"previous_names":["kiquetal/java21-workout"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kiquetal/java21-workout","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kiquetal%2Fjava21-workout","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kiquetal%2Fjava21-workout/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kiquetal%2Fjava21-workout/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kiquetal%2Fjava21-workout/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kiquetal","download_url":"https://codeload.github.com/kiquetal/java21-workout/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kiquetal%2Fjava21-workout/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34273788,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-13T02:00:06.617Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["concurrency","database","exercises","hibernate","java","java21","jpa","learning","locking","quarkus","workout"],"created_at":"2026-06-13T05:33:36.700Z","updated_at":"2026-06-13T05:33:38.888Z","avatar_url":"https://github.com/kiquetal.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Book Lending — Java 21+ Domain Modeling Playground\n\nA Quarkus project for practicing modern Java domain modeling: sealed types, records, pattern matching, and type-driven design.\n\n## Quick Start\n\n```bash\ncd book-lending\n./mvnw quarkus:dev\n```\n\nDev Services auto-starts PostgreSQL. Flyway runs migrations. Hit `http://localhost:8080/q/dev-ui` for the Dev UI.\n\n```bash\n# Lend a book\ncurl -X POST http://localhost:8080/api/lendings \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"bookId\": 1, \"memberId\": 1, \"dueDate\": \"2026-04-30\"}'\n```\n\n## Project Structure\n\n```\nsrc/main/java/dev/learning/\n├── domain/          # Entities, sealed types, value records (core model)\n├── dto/             # Request/response records (API boundary)\n├── service/         # Business logic, returns sealed results\n├── resource/        # JAX-RS endpoints, pattern match → HTTP\n└── repository/      # Panache repositories (when needed)\n```\n\n## Key Concepts Practiced\n\n### 1. Schema Migrations with Flyway\n\nFlyway owns the schema — Hibernate DDL generation is disabled.\n\n```properties\nquarkus.hibernate-orm.database.generation=none\nquarkus.flyway.migrate-at-start=true\n```\n\nVersioned SQL files in `src/main/resources/db/migration/`:\n\n```\nV1__initial_schema.sql   # Tables, constraints, indexes\nV2__seed_data.sql        # Dev data\n```\n\n**Dev UI shortcut**: When starting fresh, the Quarkus Dev UI (`/q/dev-ui`) has a Flyway card with a \"Create Initial Migration\" button that generates DDL from your entities.\n\n### 2. PanacheEntity Uses Sequences, Not BIGSERIAL\n\nThis is a common trap when writing migrations by hand. PostgreSQL has two ways to auto-generate IDs:\n\n**BIGSERIAL (identity)** — the database picks the ID on INSERT:\n```sql\n-- ❌ What you might write by hand\nCREATE TABLE book (\n    id BIGSERIAL PRIMARY KEY,   -- DB assigns 1, 2, 3, 4...\n    title VARCHAR(255) NOT NULL\n);\n```\n\n**BIGINT + SEQUENCE** — Hibernate picks the ID before INSERT:\n```sql\n-- ✅ What Panache actually expects\nCREATE SEQUENCE book_SEQ START WITH 1 INCREMENT BY 50;\nCREATE TABLE book (\n    id BIGINT NOT NULL PRIMARY KEY,   -- Hibernate assigns from sequence\n    title VARCHAR(255) NOT NULL\n);\n```\n\nWhy `INCREMENT BY 50`? Hibernate pre-fetches IDs in batches. One `SELECT nextval('book_SEQ')` gives it 50 IDs to use in memory — no DB roundtrip per insert.\n\n```\nBIGSERIAL:                          SEQUENCE (Panache):\nINSERT → DB picks id=1             nextval('book_SEQ') → 1 (gets 1-50)\nINSERT → DB picks id=2             INSERT with id=1  (no DB call)\nINSERT → DB picks id=3             INSERT with id=2  (no DB call)\n...                                 ...\n(roundtrip every insert)            INSERT with id=50 (no DB call)\n                                    nextval('book_SEQ') → 51 (gets 51-100)\n```\n\n`PanacheEntity` uses `GenerationType.SEQUENCE` internally — you can't change it without overriding the `@Id` field. The naming convention is `\u003cTABLE\u003e_SEQ`:\n\n| Entity | Table | Sequence Panache expects |\n|---|---|---|\n| `Book` | `book` | `book_SEQ` |\n| `Member` | `member` | `member_SEQ` |\n| `BookLending` | `book_lending` | `book_lending_SEQ` |\n\n**How to avoid this mistake**: Use the entity-first workflow. Write your entities, set `database.generation=drop-and-create`, run `quarkus dev`, and check the Dev UI → Hibernate ORM card for the generated DDL. It will show you the sequences. Copy that SQL into your Flyway migration, then switch to `database.generation=none`.\n\n### 3. Entities Are Classes, Not Records\n\nHibernate requires mutable classes with no-arg constructors. Records can't be entities:\n\n```java\n// ❌ Records are immutable, final, no no-arg constructor\n@Entity\npublic record Book(Long id, String title) {}\n\n// ✅ Entities must be mutable Panache classes\n@Entity\npublic class Book extends PanacheEntity {\n    public String title;\n    public String isbn;\n}\n```\n\n### 4. Records for Everything Else\n\n| Use case | Example |\n|---|---|\n| DTOs | `record LendRequest(Long bookId, ...)` |\n| Value types | `record Isbn(String value)` |\n| Sealed result variants | `record Success(BookLending lending)` |\n| Panache projections | `record BookSummary(String title, String author)` |\n| Commands | `record LendCommand(Isbn isbn, ...)` |\n\n### 5. Sealed Types as Result Types (Not Exceptions)\n\nModel business outcomes explicitly — no exceptions for expected failures:\n\n```java\npublic sealed interface LendingResult {\n    record Success(BookLending lending) implements LendingResult {}\n    record BookNotAvailable(String isbn) implements LendingResult {}\n    record MemberNotFound(Long memberId) implements LendingResult {}\n}\n```\n\nThis is Java's equivalent of F#'s discriminated unions / `Result\u003c'T, 'E\u003e`. Domain-specific sealed types are preferred over a generic `Result\u003cT\u003e` because:\n\n- Each failure variant carries its own typed data\n- The compiler enforces exhaustive handling\n- Adding a new variant breaks all unhandled switches at compile time\n- Variant names carry business meaning\n\n### 6. Pattern Matching in the Resource\n\nThe resource translates domain results to HTTP — one exhaustive switch:\n\n```java\nreturn switch (result) {\n    case Success(var lending) -\u003e Response.ok(toResponse(lending)).build();\n    case BookNotAvailable(var isbn) -\u003e Response.status(409).entity(new ErrorResponse(\"...\")).build();\n    case MemberNotFound(var id) -\u003e Response.status(404).entity(new ErrorResponse(\"...\")).build();\n};\n```\n\n### 7. Parse, Don't Validate (F#-style Value Types)\n\nValidate at construction — make invalid states unrepresentable:\n\n```java\npublic record Isbn(String value) {\n    public Isbn {\n        if (value == null || !value.matches(\"\\\\d{13}\"))\n            throw new IllegalArgumentException(\"Invalid ISBN: \" + value);\n    }\n}\n```\n\nOnce an `Isbn` exists, it's guaranteed valid. The domain only speaks in typed values, never raw strings.\n\n### 8. DTO ↔ Domain Boundary — Why Can't It Be Like F#?\n\nIn F#, you receive raw data and parse it directly into a domain type in one step:\n\n```fsharp\n// F# — the request IS the parsing step, one type does both jobs\ntype LendCommand = {\n    Isbn: Isbn           // already validated on construction\n    MemberId: MemberId   // already validated on construction\n    DueDate: DateTime\n}\n\n// JSON deserializer calls Isbn.create(\"978...\") which returns Result\u003cIsbn, Error\u003e\n// If it fails, you never get a LendCommand at all\n```\n\nIn Java, you **can't do this** because of Jackson (the JSON deserializer). Jackson needs:\n- A no-arg constructor OR a constructor with **simple types** (String, Long, etc.)\n- It doesn't know how to call `new Isbn(\"978...\")` with validation inside a compact constructor\n- If the `Isbn` constructor throws, Jackson gives you a generic 400 error with no useful message\n\nSo you're forced into two steps:\n\n```\nStep 1: JSON → DTO (raw types, Jackson can handle this)\nStep 2: DTO → Domain types (you parse and validate here)\n```\n\nConcretely:\n\n```java\n// STEP 1 — dto/LendRequest.java\n// Jackson deserializes JSON into this. Simple types only.\npublic record LendRequest(\n    @NotBlank String isbn,       // just a String — Jackson is happy\n    @NotNull Long memberId,      // just a Long — Jackson is happy\n    @NotNull LocalDate dueDate\n) {}\n\n// STEP 2 — resource parses DTO into domain types\n@POST\npublic Response lend(@Valid LendRequest request) {\n    // This is the F# \"parse\" moment — raw strings become domain types\n    var command = new LendCommand(\n        new Isbn(request.isbn()),           // validates here\n        new MemberId(request.memberId()),   // validates here\n        request.dueDate()\n    );\n    var result = lendingService.lend(command);\n    // ...\n}\n\n// domain/LendCommand.java — the service only sees this\npublic record LendCommand(Isbn isbn, MemberId memberId, LocalDate dueDate) {}\n```\n\nThe service receives `LendCommand` with **already-validated domain types**:\n\n```java\n// Service never sees raw strings. Isbn and MemberId are guaranteed valid.\npublic LendingResult lend(LendCommand command) {\n    // command.isbn() → Isbn, not String\n    // command.memberId() → MemberId, not Long\n    // impossible to have invalid data here\n}\n```\n\n**Why F# doesn't have this problem**: F#'s serializers (like Thoth.Json) support custom decoders — you write a function that parses `string → Result\u003cIsbn, Error\u003e` and wire it into deserialization. The parsing IS the deserialization. Java's Jackson doesn't work that way — it constructs objects first, validates later.\n\n**The mental model**:\n\n```\nF#:    JSON ──parse──→ Domain type (one step, decoder does validation)\nJava:  JSON ──jackson──→ DTO (dumb data) ──you parse──→ Domain type (two steps)\n```\n\nThe resource layer is where the two-step gap lives. It's the cost of using Jackson. Everything after that boundary is the same as F# — validated types, no raw strings, no nulls.\n\n**The flow through layers**:\n\n```\nClient JSON → LendRequest (DTO, raw)\n            → LendCommand (domain, validated)     ← parsing boundary\n            → LendingService (works with domain)\n            → LendingResult (sealed outcome)\n            → LendingResponse (DTO, shaped for client)\n            → Client JSON\n```\n\n- **DTOs** live in `dto/`, face the outside world, carry validation annotations\n- **Domain types** live in `domain/`, guaranteed valid after construction\n- **The resource** is the translator — the parsing boundary\n- **The service** never sees DTOs, never knows about HTTP\n\n### 9. Validation Layers\n\n| Layer | Purpose |\n|---|---|\n| Bean Validation on DTOs | Early, user-friendly error messages (`@NotNull`, `@Size`) |\n| Value type constructors | Domain integrity (`Isbn`, `Email`) |\n| `@Column` annotations | Documents entity-to-table mapping |\n| DB constraints (Flyway SQL) | Last line of defense — always there |\n\n### 10. Panache Type Witness Quirk\n\n`findByIdOptional` returns `Optional\u003cObject\u003e`. Use a type witness to get the right type:\n\n```java\n// ❌ Returns Optional\u003cObject\u003e\nMember.findByIdOptional(id)\n\n// ✅ Returns Optional\u003cMember\u003e\nMember.\u003cMember\u003efindByIdOptional(id)\n```\n\n### 11. Java Type Inference in Optional Chains\n\nWhen chaining `map`/`orElseGet` with sealed types, Java infers the type from the first branch and locks it. Use a type witness on `map` to widen:\n\n```java\n// ❌ Compiler infers map returns Optional\u003cSuccess\u003e, orElseGet fails\n.map(book -\u003e new LendingResult.Success(lending))\n.orElseGet(() -\u003e new LendingResult.BookNotAvailable(isbn))\n\n// ✅ Type witness tells compiler to use the parent type\n.\u003cLendingResult\u003emap(book -\u003e new LendingResult.Success(lending))\n.orElseGet(() -\u003e new LendingResult.BookNotAvailable(isbn))\n```\n\n### 12. Hibernate Dirty Checking — `persist()` vs Managed Entities\n\nWhen you **create** a new entity, you must call `persist()` — Hibernate doesn't know about it yet:\n\n```java\nvar member = new Member();              // just a Java object, not in DB\nmember.name = command.name();\nmember.email = command.email().value();\nmemberRepository.persist(member);       // NOW Hibernate tracks it and saves to DB\n```\n\nWhen you **update** an existing entity, Hibernate already loaded it — it's \"managed\". Any field change is auto-flushed at transaction commit:\n\n```java\nmemberRepository.findByMemberId(...)    // loads from DB — Hibernate \"manages\" it\n    .map(member -\u003e {\n        member.name = command.name();   // mutate the managed entity\n        member.email = ...;             // Hibernate tracks these changes\n        // no persist() needed — auto-flushed at commit\n        return new Success(member);\n    })\n```\n\n| | Create | Update |\n|---|---|---|\n| Entity comes from | `new Entity()` — not managed | `repository.find()` — managed |\n| Needs `persist()`? | Yes | No — dirty checking handles it |\n| Needs `@Transactional`? | Yes | Yes |\n\n\"Managed\" = Hibernate loaded it and is watching it. Change a field → Hibernate detects the diff → flushes the UPDATE at commit. No explicit save needed.\n\n### 13. `@Transactional` Belongs on the Service, Not the Resource\n\nThe resource is a translator (DTO ↔ HTTP). The service owns the business operation. The transaction wraps the business operation — so `@Transactional` goes on the service.\n\n```\n  ┌─────────────────────────────────────────────────────────────────┐\n  │                        REQUEST FLOW                             │\n  │                                                                 │\n  │   Client                                                        │\n  │     │                                                           │\n  │     ▼                                                           │\n  │   ┌──────────────────────────────────┐                          │\n  │   │  Resource (JAX-RS)               │  No @Transactional       │\n  │   │  • Receives JSON                 │  • Parses DTO → Command  │\n  │   │  • Returns HTTP Response         │  • Pattern matches result│\n  │   └──────────────┬───────────────────┘                          │\n  │                  │ LendCommand (validated domain types)          │\n  │                  ▼                                               │\n  │   ┌──────────────────────────────────┐                          │\n  │   │  Service                         │  @Transactional          │\n  │   │  ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │                          │\n  │   │    TX BEGIN                      │                          │\n  │   │  │ • repo.find() → Entity  ◄────┼── Hibernate manages it   │\n  │   │    • Business rules              │                          │\n  │   │  │ • repo.persist() if new       │                          │\n  │   │    • Entity → Record (convert)   │                          │\n  │   │  │ TX COMMIT (auto-flush)        │                          │\n  │   │  └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │                          │\n  │   └──────────────┬───────────────────┘                          │\n  │                  │ LendingResult (sealed, contains records)      │\n  │                  ▼                                               │\n  │   ┌──────────────────────────────────┐                          │\n  │   │  Resource (pattern match)        │                          │\n  │   │  Success → 200 OK               │                          │\n  │   │  NotFound → 404                  │                          │\n  │   │  AlreadyLent → 409              │                          │\n  │   └──────────────────────────────────┘                          │\n  └─────────────────────────────────────────────────────────────────┘\n```\n\n**Why not on the resource?**\n\n```\n  ❌ @Transactional on Resource          ✅ @Transactional on Service\n  ┌─────────────────────────┐            ┌─────────────────────────┐\n  │ Resource                │            │ Resource                │\n  │ ┌─ TX ────────────────┐ │            │ (no TX)                 │\n  │ │ parse DTO           │ │            │  parse DTO              │\n  │ │ call service        │ │            │  call service ──┐       │\n  │ │ build Response ◄──┐ │ │            │  match result   │       │\n  │ │ (entity still      │ │ │            │  build Response │       │\n  │ │  alive here —      │ │ │            └─────────────────┘       │\n  │ │  lazy load risk!)  │ │ │                              │       │\n  │ └────────────────────┘ │            ┌───────────────────▼─────┐\n  │  TX open too long      │            │ Service                 │\n  │  Entity leaks to HTTP  │            │ ┌─ TX ───────────────┐  │\n  └─────────────────────────┘            │ │ entities live HERE │  │\n                                         │ │ convert → records  │  │\n                                         │ └───────────────────┘  │\n                                         │  records escape (safe) │\n                                         └────────────────────────┘\n```\n\nThe problems with `@Transactional` on the resource:\n- Transaction stays open while building the HTTP response — longer than needed\n- Entities leak into the resource — tempts you to skip the record conversion\n- `LazyInitializationException` risk if you access unloaded relations in the response builder\n- Mixes concerns — the resource shouldn't know a database exists\n\n**The rule**: entities live and die inside the `@Transactional` service method. What comes out is an immutable record inside a sealed result. The resource only sees records, never entities.\n\n```java\n// ✅ Service owns the transaction\n@ApplicationScoped\npublic class LendingService {\n\n    @Inject LendingRepository lendingRepository;\n    @Inject MemberRepository memberRepository;\n\n    @Transactional\n    public LendingResult lend(LendCommand command) {\n        var member = memberRepository.findByMemberId(command.memberId())\n            .orElse(null);\n        if (member == null)\n            return new LendingResult.MemberNotFound(command.memberId());\n\n        // ... business rules, persist, convert entity → record ...\n        return new LendingResult.Success(/* BookLendingResult record */);\n    }\n}\n```\n\n**F# parallel**: In F#, your workflow function is the unit of work. You'd wrap it in a `transaction { }` computation expression or let the infrastructure handle it. The HTTP handler just calls the workflow and maps the result. Same idea — the boundary between \"database-aware\" and \"HTTP-aware\" is the service method.\n\n### 14. Test Configuration — Separate `application.properties`\n\nQuarkus looks for `src/test/resources/application.properties` and uses it **instead of** the main one during tests. This lets you isolate test settings without polluting your dev/prod config.\n\n**Why bother?** Without it, your tests inherit whatever's in the main config. If you enable Flyway, switch Hibernate to `validate`, or change logging in main — your tests break. A separate test config keeps them independent.\n\n**File layout:**\n\n```\nsrc/main/resources/application.properties    ← dev + prod\nsrc/test/resources/application.properties    ← tests only (overrides main)\n```\n\n**What to put in the test config:**\n\n```properties\n# ── Dev Services (test container) ──\n# Quarkus auto-starts a PostgreSQL container for tests.\n# These settings control THAT container, not your real DB.\nquarkus.datasource.db-kind=postgresql\nquarkus.devservices.enabled=true\nquarkus.datasource.devservices.image-name=postgres:17       # pin version\nquarkus.datasource.devservices.db-name=book_lending_test    # distinct name\nquarkus.datasource.devservices.username=test\nquarkus.datasource.devservices.password=test\n\n# ── Hibernate ──\n# drop-and-create = clean schema every test run\nquarkus.hibernate-orm.schema-management.strategy=drop-and-create\nquarkus.hibernate-orm.log.sql=true    # see queries in test output\n\n# ── Flyway ──\n# Off — Hibernate manages schema via drop-and-create\nquarkus.flyway.migrate-at-start=false\n```\n\n**Key differences from main config:**\n\n| Setting | Main (dev/prod) | Test |\n|---|---|---|\n| Hibernate schema | `%dev.drop-and-create` / `%prod.none` | `drop-and-create` (always clean) |\n| Flyway | Depends on environment | Off — Hibernate owns the schema |\n| Dev Services image | Default (latest) | Pinned (`postgres:17`) for reproducibility |\n| DB name | Default | `book_lending_test` — distinct from dev |\n| SQL logging | Off | On — useful for debugging test failures |\n\n**How it works at runtime:**\n\n```\n./mvnw quarkus:dev  → src/main/resources/application.properties (%dev profile)\n./mvnw test         → src/test/resources/application.properties (overrides main)\n```\n\nBoth spin up a Testcontainers PostgreSQL via Dev Services, but with independent settings. The test container is ephemeral — created before tests, destroyed after.\n\n**Test types and what they need:**\n\n| Test type | Needs `@QuarkusTest`? | Needs Dev Services? | Example |\n|---|---|---|---|\n| Pure domain (value types, sealed results) | No | No | `ValueTypeTest`, `SealedResultTest` |\n| Integration (REST endpoints) | Yes | Yes (auto) | `BookItemResourceTest` |\n\nPure domain tests don't touch the container at all — they run in milliseconds. Only `@QuarkusTest` classes trigger Dev Services and the full Quarkus lifecycle.\n\n## Tech Stack\n\n- Java 21+\n- Quarkus 3.34\n- Hibernate ORM with Panache\n- RESTEasy Reactive + Jackson\n- PostgreSQL (via Dev Services)\n- Flyway\n- Hibernate Validator\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkiquetal%2Fjava21-workout","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkiquetal%2Fjava21-workout","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkiquetal%2Fjava21-workout/lists"}