<?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: Themba Ngobeni</title>
    <description>The latest articles on DEV Community by Themba Ngobeni (@themba).</description>
    <link>https://dev.to/themba</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%2F906713%2Ffbd99458-7931-4aa7-b28f-0aae7db26ad7.jpeg</url>
      <title>DEV Community: Themba Ngobeni</title>
      <link>https://dev.to/themba</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/themba"/>
    <language>en</language>
    <item>
      <title>Faker Doesn't Know Your Entities Are Related, So I Built Something That Does</title>
      <dc:creator>Themba Ngobeni</dc:creator>
      <pubDate>Mon, 07 Sep 2026 21:48:44 +0000</pubDate>
      <link>https://dev.to/themba/faker-doesnt-know-your-entities-are-related-so-i-built-something-that-does-2jgn</link>
      <guid>https://dev.to/themba/faker-doesnt-know-your-entities-are-related-so-i-built-something-that-does-2jgn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Annotate your JPA entities with &lt;code&gt;@Seed(count = n)&lt;/code&gt;, start your Spring Boot app in a dev profile, and the tables are populated in dependency order with realistic, constraint-respecting data. No seed script. &lt;code&gt;io.github.thembatman0:synthforge-spring:0.1.0&lt;/code&gt; on Maven Central.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You've added a second entity to the schema, wired up a &lt;code&gt;@ManyToOne&lt;/code&gt;, and gone back to your seed script to generate fifty more rows. Ninety seconds later, the app refuses to start: unique constraint violation, somewhere inside a loop you wrote three weeks ago at 11pm. You fix it. You restart. A different field breaks a different constraint.&lt;/p&gt;

&lt;p&gt;This is the exact moment every Spring Boot developer eventually meets the real limit of tools like Faker. They're brilliant at generating a name, an email, an address. They have no idea the &lt;code&gt;Payment&lt;/code&gt; sitting in front of them needs a &lt;code&gt;Counterparty&lt;/code&gt; to already exist.&lt;/p&gt;

&lt;p&gt;So you do what everyone does: hand-write the wiring. Create parents first. Hold onto their generated IDs. Wire them into children. Hope you didn't just violate a &lt;code&gt;@NotNull&lt;/code&gt; somewhere in the process. It works, for a while. Then the schema changes, and the script quietly stops matching reality until the next 3am debugging session finds out the hard way.&lt;/p&gt;

&lt;p&gt;I hit this enough times that I stopped patching the script and looked at the actual problem: the information needed to seed this correctly already exists. It's sitting right there in the entity, in the annotations you already wrote. &lt;code&gt;@ManyToOne&lt;/code&gt;, &lt;code&gt;@NotNull&lt;/code&gt;, &lt;code&gt;@Column(unique = true)&lt;/code&gt;, JPA already knows the shape of your data. Nothing should need to be told that twice.&lt;/p&gt;

&lt;p&gt;That became SynthForge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;Instead of writing a script that generates data, you annotate the entity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt; &lt;span class="nd"&gt;@Seed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Counterparty&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* fields only */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Entity&lt;/span&gt; &lt;span class="nd"&gt;@Seed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Payment&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@ManyToOne&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optional&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Counterparty&lt;/span&gt; &lt;span class="n"&gt;counterparty&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the app in a dev profile. Both tables populate, correctly ordered. Restart it and tables that already have rows are skipped. No seed method. No calling code, anywhere. The entity is the seed script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in two minutes
&lt;/h2&gt;

&lt;p&gt;The repo ships a small demo app (&lt;code&gt;Counterparty&lt;/code&gt; and &lt;code&gt;Payment&lt;/code&gt; on an in-memory H2 database), so you can see it work before touching your own project. You need Java 21 and Maven.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ThembaTman0/synthforge.git
&lt;span class="nb"&gt;cd &lt;/span&gt;synthforge
mvn &lt;span class="nt"&gt;-pl&lt;/span&gt; synthforge-demo &lt;span class="nt"&gt;-am&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-DskipTests&lt;/span&gt;
mvn &lt;span class="nt"&gt;-pl&lt;/span&gt; synthforge-demo spring-boot:run &lt;span class="nt"&gt;-Dspring-boot&lt;/span&gt;.run.profiles&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Along with the SQL it logs, you should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SynthforgeAutoConfiguration : Seeding with random seed 1593487635758507864 (set synthforge.seed to this value to reproduce the run)
SynthforgeAutoConfiguration : Seeded 50 Counterparty rows
SynthforgeAutoConfiguration : Seeded 200 Payment rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The seed value changes on every run. Set &lt;code&gt;synthforge.seed&lt;/code&gt; to a fixed number and you get identical data each time, which helps when you're debugging. The demo has no web layer, so it seeds, logs and exits.&lt;/p&gt;

&lt;p&gt;To use it in your own app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the dependency (live on Maven Central):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.github.thembatman0&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;synthforge-spring&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;0.1.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or, with Gradle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'io.github.thembatman0:synthforge-spring:0.1.0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Put &lt;code&gt;@Seed(count = n)&lt;/code&gt; on your entities.&lt;/li&gt;
&lt;li&gt;List the profiles that should seed under &lt;code&gt;synthforge.enabled-profiles&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;synthforge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled-profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;dev&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the app in one of those profiles and the tables fill in the right order, parents before children. Restarts against a database that already has rows skip seeding, so nothing is duplicated.&lt;/p&gt;

&lt;p&gt;It's tested in CI on Spring Boot 4.1 and 3.5. Spring Boot 3.5 apps still need Java 21 or newer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually happening underneath
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Entity scanning.&lt;/strong&gt; SynthForge reads JPA-managed attributes through the &lt;code&gt;jakarta.persistence.metamodel.Metamodel&lt;/code&gt; API, not raw reflection. That distinction matters more than it sounds: only real persistent fields ever get touched, nothing that merely resembles one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relationship ordering.&lt;/strong&gt; From the owning-side &lt;code&gt;@ManyToOne&lt;/code&gt;/&lt;code&gt;@OneToOne&lt;/code&gt; relationships, it builds a dependency graph and topologically sorts it. Parent rows exist before a child is ever generated to reference them. This is precisely where hand-written scripts fail first: the moment a second level of nesting shows up, manual ID-tracking becomes its own source of bugs, the thing you're debugging instead of your actual feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constraint-aware generation.&lt;/strong&gt; &lt;code&gt;@NotNull&lt;/code&gt;, &lt;code&gt;@Size&lt;/code&gt;, &lt;code&gt;@Email&lt;/code&gt;, and a set of field-name heuristics (&lt;code&gt;email&lt;/code&gt;, &lt;code&gt;iban&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;) drive realistic values through Datafaker. Hit a &lt;code&gt;@Column(unique = true)&lt;/code&gt; field, and instead of a constraint violation forty rows in, you get a bounded retry loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent restarts.&lt;/strong&gt; A table that already has rows gets skipped. Restart against a persistent database as many times as you want; nothing duplicates.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Isn't this just Instancio?"
&lt;/h2&gt;

&lt;p&gt;Fair question, and worth answering directly rather than dodging it. Instancio, and its &lt;code&gt;instancio-jpa&lt;/code&gt; extension, gives you an API to call from inside a test: build a graph, persist it, for that one test. It's genuinely good at that job.&lt;/p&gt;

&lt;p&gt;SynthForge isn't called from anywhere. Annotate the entity, start the app in an enabled profile, and the database is already populated before your code runs a single line. Different tool, because it solves a different moment: application startup, not a single test method.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it isn't the right tool
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You want a fresh object graph inside a single test method. &lt;a href="https://github.com/instancio/instancio" rel="noopener noreferrer"&gt;Instancio&lt;/a&gt; is built for that.&lt;/li&gt;
&lt;li&gt;You need data in production. Seeding is for dev and test profiles only, and stays off unless you list a profile.&lt;/li&gt;
&lt;li&gt;Your model relies on &lt;code&gt;@OneToMany&lt;/code&gt;/&lt;code&gt;@ManyToMany&lt;/code&gt; collections or composite keys. Those fields are skipped in this release.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;Live on Maven Central, MIT licensed, at version 0.1.0. Repo: &lt;a href="https://github.com/ThembaTman0/synthforge" rel="noopener noreferrer"&gt;github.com/ThembaTman0/synthforge&lt;/a&gt;. Feedback and issues are welcome there.&lt;/p&gt;

&lt;p&gt;It won't fix your architecture and it won't make decisions for you. But the next time you add a foreign key at 11pm, there's a decent chance it means you get to go to bed instead.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
