<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rahkel Pekins</title>
    <description>The latest articles on DEV Community by Rahkel Pekins (@pekins1).</description>
    <link>https://dev.to/pekins1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4192496%2F97040b7b-586f-4aec-8dfe-1399c2d54618.jpg</url>
      <title>DEV Community: Rahkel Pekins</title>
      <link>https://dev.to/pekins1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pekins1"/>
    <language>en</language>
    <item>
      <title>My Tests Were Green on the Wrong Database — Moving to Real PostgreSQL with Flyway</title>
      <dc:creator>Rahkel Pekins</dc:creator>
      <pubDate>Sun, 25 Oct 2026 18:55:28 +0000</pubDate>
      <link>https://dev.to/pekins1/my-tests-were-green-on-the-wrong-database-moving-to-real-postgresql-with-flyway-4lo6</link>
      <guid>https://dev.to/pekins1/my-tests-were-green-on-the-wrong-database-moving-to-real-postgresql-with-flyway-4lo6</guid>
      <description>&lt;p&gt;In Part 1 I ended with a question I was genuinely sitting with: REST controllers first, or persistence first?&lt;/p&gt;

&lt;p&gt;Persistence won — in the project's own ROADMAP order. And it turned out to be the right call, because it exposed something uncomfortable: my repository tests were green, but they were green on the wrong database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem: H2 was hiding the truth&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Up to this point, the app's tests ran against H2, an in-memory stand-in. That's fine for fast unit tests. It's not fine as proof that your schema works, because the database your app will actually run on is PostgreSQL — different engine, different constraint behavior, different failure modes.&lt;/p&gt;

&lt;p&gt;A test passing on H2 only proves the code works in a world that doesn't exist. So Phase 3 had one job: make the persistence layer real, reproducible, and proven on the real engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shipped: four PRs, one evening&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PR #13 — ROADMAP close-out, and one docs-vs-code call: my ROADMAP listed a POOR book condition that my enum didn't have, so I changed the code, not the doc. POOR was appended to BookCondition after FAIR.&lt;br&gt;
PR #14 — Flyway plus the baseline migration.&lt;br&gt;
PR #15 — Environment-based database config and documentation.&lt;br&gt;
PR #16 — Repository tests on real PostgreSQL, plus a matching CI service.&lt;br&gt;
48 tests when the evening ended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flyway owns the schema now&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The baseline migration, V1_&lt;em&gt;initial&lt;/em&gt;_schema.sql, creates all five tables and mirrors the JPA entities exactly — including named constraints, so a failure message points at a real, findable thing instead of an anonymous auto-generated name:&lt;/p&gt;

&lt;p&gt;CREATE TABLE book_copies (&lt;br&gt;
    id             BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,&lt;br&gt;
    barcode        VARCHAR(255) NOT NULL,&lt;br&gt;
    is_available   BOOLEAN NOT NULL,&lt;br&gt;
    times_borrowed INTEGER NOT NULL,&lt;br&gt;
    condition      VARCHAR(255) NOT NULL,&lt;br&gt;
    acquired_date  DATE NOT NULL,&lt;br&gt;
    book_id        BIGINT NOT NULL REFERENCES books (id),&lt;br&gt;
    owner_id       BIGINT NOT NULL REFERENCES library_members (id),&lt;br&gt;
    visibility     VARCHAR(255) NOT NULL,&lt;br&gt;
    CONSTRAINT book_copy_barcode UNIQUE (barcode)&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;That owner_id foreign key is the ownership work from Part 1, now real in the database — a copy cannot exist without a member who owns it. The other tables follow the same pattern: library_members with a named library_member_email unique constraint, books with book_isbn, and borrowings linking book, copy, and borrower.&lt;/p&gt;

&lt;p&gt;The key decision is in the profile that runs against it:&lt;/p&gt;

&lt;p&gt;spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/library}&lt;br&gt;
spring.datasource.username=${DB_USERNAME:library}&lt;br&gt;
spring.datasource.password=${DB_PASSWORD:}&lt;br&gt;
spring.datasource.driver-class-name=org.postgresql.Driver&lt;br&gt;
spring.flyway.enabled=true&lt;br&gt;
spring.jpa.hibernate.ddl-auto=validate&lt;/p&gt;

&lt;p&gt;ddl-auto=validate is the whole philosophy in one line. Hibernate no longer creates or updates the schema — Flyway applies the migration, and Hibernate checks the entities agree with it, refusing to start if they don't. The migration is the source of truth. Never edit an applied migration; schema changes ship as new V2_&lt;em&gt;..., V3&lt;/em&gt;_... files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Config that doesn't leak secrets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Database settings moved to environment variables with a .env.example in the repo, and the README gained a "Database Setup (PostgreSQL)" section so anyone cloning the project can reproduce the setup from scratch. No credentials live in the code.&lt;/p&gt;

&lt;p&gt;One honest detail: there's no Docker on my machine, so the usual Testcontainers route was out. PostgreSQL 17 runs natively on my machine instead. Not the fashionable path — the path that actually works in my setup. The CI side still gets the standard treatment: a postgres:17 service with a health check, matching the test setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The test that finally proves something&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only one test class moved off H2 — BookCopyRepositoryTest. The other 45 tests stay where they are; this was never about moving everything, it was about proving the repository layer against the real engine.&lt;/p&gt;

&lt;p&gt;The gotcha that would have silently undone the whole thing: @DataJpaTest swaps in an embedded database by default. Without one annotation, my "PostgreSQL test" would have been an H2 test wearing a PostgreSQL costume:&lt;/p&gt;

&lt;p&gt;@DataJpaTest&lt;br&gt;
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)&lt;br&gt;
@TestPropertySource(locations = "classpath:application-pgtest.properties")&lt;br&gt;
class BookCopyRepositoryTest {&lt;/p&gt;

&lt;p&gt;Replace.NONE is the line that makes it real. The test profile points at a dedicated library_test database, with Flyway on and ddl-auto=validate — so the test schema is built by the same migration the app uses, not by Hibernate improvising one.&lt;/p&gt;

&lt;p&gt;And the test I'm proudest of is a failure. Saving a second copy with a duplicate barcode throws a PersistenceException whose root cause names the real book_copy_barcode unique constraint. That's proof the constraint exists on the real engine — not just in an annotation, not just in H2's approximation of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;An in-memory database is a stand-in, not a substitute. It's great for speed, dangerous as proof.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Named constraints are documentation. When a test fails, it should tell you exactly which rule broke.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;validate beats create-drop once a schema exists. Letting the ORM silently rebuild your schema hides drift between your entities and your migrations — the exact drift that breaks you in production.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The unglamorous work is the platform. Nobody demos a migration file. But without it, every feature built on top is sitting on a schema nobody can reproduce.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What's next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Persistence is done: externalized config, reproducible migrations, integration tests on the real database. The next chapter is the REST API — controllers and global exception handling on top of the service layer, so this stops being a library only other code can use and starts becoming a platform members can actually call.&lt;/p&gt;

&lt;p&gt;I'm working on that today, and it gets its own post next week — after it's actually finished, not before. That's the rule for this series: shipped first, posted second.&lt;/p&gt;

&lt;p&gt;If you've moved a project from H2 (or SQLite, or any in-memory stand-in) to a real database, what broke first — the schema, the tests, or your assumptions? I'd genuinely like to hear it.&lt;/p&gt;

&lt;p&gt;Code, if you want to follow along or tell me what you'd do differently:&lt;br&gt;
GitHub - github.com/Pekins1/LibraryManagementSystem&lt;/p&gt;

&lt;p&gt;— Rahkel&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>postgressql</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I'm Building a Peer-to-Peer Library Platform in Public — From a Legacy Java Monolith to Spring Boot</title>
      <dc:creator>Rahkel Pekins</dc:creator>
      <pubDate>Sun, 18 Oct 2026 18:13:50 +0000</pubDate>
      <link>https://dev.to/pekins1/im-building-a-peer-to-peer-library-platform-in-public-from-a-legacy-java-monolith-to-spring-boot-2m8l</link>
      <guid>https://dev.to/pekins1/im-building-a-peer-to-peer-library-platform-in-public-from-a-legacy-java-monolith-to-spring-boot-2m8l</guid>
      <description>&lt;p&gt;Hi, I'm Rahkel. I'm a backend developer, and I'm building a software project solo, in public.&lt;/p&gt;

&lt;p&gt;This series is the log of that build — twice a week, what I shipped, the design decisions behind it, what broke, and what's next. No highlight reel. If a test fails on a Saturday and I have to leave for work before I can fix it, that goes in the log too. (That has already happened. More than once.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The idea&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm turning my old Library Management System into a peer-to-peer library platform.&lt;/p&gt;

&lt;p&gt;Instead of one library owning every book, individuals catalog their own personal collections. Other members can discover those books and borrow the copies the owners choose to make public. Owners will also be able to give books away to interested members, or donate them to an actual library. And libraries and organizations can run the same lending functionality for their own patrons.&lt;br&gt;
That's the destination. This series is about the unglamorous part: rebuilding the foundation so that destination is actually possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it started&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Honestly? I cringe a little looking at the original code — and I've learned to take that as a good sign. It means my standards moved.&lt;/p&gt;

&lt;p&gt;🔴 Before:&lt;/p&gt;

&lt;p&gt;A console app built around one massive Library class&lt;br&gt;
Borrowing, duplicates, genres, and limits all handled in the same place&lt;br&gt;
Books tracked in in-memory lists and maps — close the app, lose the state&lt;br&gt;
No separation between a book title and the physical copies of it&lt;br&gt;
It worked as a learning project. It could never work as a platform where real people lend each other real books.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it is now&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🟢 Now:&lt;/p&gt;

&lt;p&gt;Rebuilt on Java 21 + Spring Boot, and the app actually boots and runs&lt;br&gt;
A layered architecture: dedicated services (BookService, BorrowService, LibraryMemberService), Spring Data JPA repositories, DTOs for requests, and @Transactional workflows&lt;br&gt;
Books modeled as titles with multiple physical copies, each tracked on its own&lt;br&gt;
Tests at every step — 47 and counting&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The part I just finished: ownership and visibility&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's something I didn't appreciate until I started building it: a peer-to-peer library is impossible until the code knows who owns what.&lt;br&gt;
When I started this phase, my book copies belonged to nobody and were effectively visible to everybody — exactly backwards for a platform where people lend out their own collections. So the most recent chapter of the build (Step 2, just completed) was:&lt;/p&gt;

&lt;p&gt;A new LibraryMember entity — the platform member who owns book copies, with a MemberType of INDIVIDUAL or ORGANIZATION, because a person and a library won't use this the same way&lt;br&gt;
A member-creation path with proper validation and a case-insensitive email lookup&lt;br&gt;
CopyVisibility on every copy:&lt;br&gt;
public enum CopyVisibility {&lt;br&gt;
    PRIVATE,&lt;br&gt;
    PUBLIC&lt;br&gt;
}&lt;br&gt;
PRIVATE is the default — owner only, cannot be borrowed. PUBLIC means other members can discover the copy and borrow it. Privacy first, sharing by choice.&lt;/p&gt;

&lt;p&gt;A discovery query that only ever surfaces available PUBLIC copies — private copies don't just fail to borrow, they don't show up at all&lt;/p&gt;

&lt;p&gt;One design decision I made on purpose, and would defend: the owner is not the existing Borrower entity. A member owns and lends copies. A borrower is an external patron of a library — a different job. Collapsing those two into one entity would have been faster for a week and messier forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens in this series&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every post follows the same simple shape:&lt;br&gt;
Shipped: what actually landed in the code, with the PR to prove it&lt;br&gt;
Decided: one design call, and why I made it&lt;br&gt;
Next: what I'm building next&lt;/p&gt;

&lt;p&gt;I'm doing this for three reasons: to hold my own standards up where people can see them, to find a community of developers building similar things, and because I'm open to collaborations — feedback on the design is genuinely welcome, especially when you think I got a call wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ownership and visibility were groundwork. The platform layer they were groundwork for — exposing this as a real API with real persistence, so members can actually use it — is the next chapter, and I'm deciding the exact order of that work right now.&lt;/p&gt;

&lt;p&gt;So I'll end with the question I'm actually sitting with:&lt;/p&gt;

&lt;p&gt;If you were building this, would you put the REST controllers on top of the current service layer first, or lock down the persistence/database layer first — and why?&lt;/p&gt;

&lt;p&gt;Follow the series if you want to watch a legacy monolith become a platform, one decision at a time.&lt;/p&gt;

&lt;p&gt;Code, if you want to follow along or tell me what you'd do differently: github.com/Pekins1/LibraryManagementSystem&lt;/p&gt;

</description>
      <category>java</category>
      <category>buildinpublic</category>
      <category>springboot</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
