DEV Community

Cover image for I read my competitor's generated code. It was better than mine in six places.
Gilles Bernard
Gilles Bernard

Posted on

I read my competitor's generated code. It was better than mine in six places.

I build a browser-based Spring Boot generator. A competitor, Bootify, has been around longer and is listed in awesome-java. I had never actually read their output.

So I generated the same domain model in both tools, unzipped the two projects side by side, and went through them file by file. This post is what I found, including the parts where their output is better than mine.

Disclosure up front: I am the author of SpringBoot Generator, one of the two tools compared here. Everything below is checkable — generate the same model in both, both have a free tier, and diff the ZIPs yourself.

The method

Same domain on both sides, deliberately trivial:

  • Customerid, name (String, 255, required)
  • Orderid, label (String, 255, required), a required many-to-one to Customer

Order is a reserved SQL keyword. That was on purpose: escaping reserved table names is a real correctness question and a cheap way to see whether a generator thinks about SQL at all.

I ticked every option each tool offered on the tier I used, downloaded both ZIPs, and compared.

Where the comparison is unfair, stated up front

The two inputs are not identical, and pretending otherwise would invalidate everything after it. Four differences:

  • their model declares the foreign keys in the opposite direction;
  • theirs is on PostgreSQL, mine on MySQL;
  • theirs is a Gradle build, mine Maven (both tools support both);
  • their fields were not marked required, so their validation annotations are thinner than mine for reasons that have nothing to do with the generator.

So validation depth and SQL column types are not comparable here. Topology, layering, tests, tooling and documentation are.

The headline numbers

SpringBoot Generator Bootify
ZIP size 68 KB 196 KB
Files 71 95
README lines 200, across 3 files 57, one file

The size difference is mostly their gradle-wrapper.jar. It means nothing.

The one structural difference

This is the thing that actually distinguishes the two, and it is not a checkbox.

Bootify emits a single deployable artifact. The generated Angular app lives in src/main/webapp/ and is served by Spring through an AngularForwardController. One build, one JAR, one deployment.

Mine emits two projects, backend/ and frontend/, each with its own README, its own build, and no knowledge of where the other one is deployed beyond a CORS config and a base URL.

Neither is correct in the abstract. A single artifact is genuinely simpler to ship — one pipeline, no CORS, no origin config. Two projects is what you want when the frontend goes on a CDN, when a second client appears, or when the two sides have different release
cadences.

I bet on the second. It is a bet, not a feature.

Where Bootify's output is better than mine

Six of them. This is the part I did not expect to write.

Audit columns. Their entities carry dateCreated and lastUpdated, maintained automatically, with updatable = false where it matters. Mine have nothing. On a real project this is one of the first things you add by hand.

Referential integrity on delete. They generate events/BeforeDelete* handlers and a ReferencedException, so deleting a parent that still has children fails cleanly. Mine does not address it, so you get whatever the database decides.

A real frontend shell. Theirs ships a header, a home page, an error page, a global error-handler, a title-strategy, an input-errors component, a favicon, a logo and SCSS. Mine generates CRUD screens and nothing around them — no header, no home, no error
page.

Separate templates. Their Angular components use .html + .scss files. Mine inlines templates in the .ts. Theirs is the idiomatic choice.

i18n attributes on their form components, factored through a reusable app-input-row. Mine writes each form field out longhand, in English only.

open-in-view: false in their config. Minor, but it is the correct default and I had not set it.

Where my output is ahead

Generated tests. My ZIP contains 8 written @Test methods per service with real assertions, plus a Testcontainers integration test of 136 lines. Their free ZIP contained no src/test/ directory at all.

This is the number that needs the asterisk, so here it is: Bootify sells test generation on their paid plan. I compared a free ZIP against my own output with the paid options enabled. The honest claim is not "they don't generate tests" — it is that I have not verified what their paid tier produces, and neither should you take my word for it.

Deployment artifacts. A Dockerfile, a docker-compose.yml that starts the application and the database, and a .dockerignore. Theirs has no Dockerfile; its compose file starts the database only, wired through spring-boot-docker-compose in start-only mode. Theirs is better for ./gradlew bootRun on a laptop. Mine is better for putting the thing on a server.

Environment profiles. application-dev.properties and application-prod.properties, with ddl-auto set to create-drop and validate respectively. Theirs has one application.yml with ddl-auto: update — which is the setting you least want pointed at a production database.

A SQL schema file. resources/sql/script.sql ships with the project. Theirs relies on Hibernate to create the schema.

Documentation. 200 lines across three READMEs against 57 in one.

Where it's a tie

The reserved keyword. Both handle Order, differently. They escape it at the annotation level: @Table(name = "\"order\""). I set hibernate.globally_quoted_identifiers=true in the properties. Both work; theirs is more surgical, mine is harder to accidentally undo.

ID generationIDENTITY on my side, @SequenceGenerator on theirs. Both fine.

DTO mapping — I generate explicit mappers behind an IEntityMapper interface, they map inside the service. More files on my side, fewer indirections on theirs.

Lazy loading. Their @ManyToOne associations are explicitly fetch = FetchType.LAZY. Mine were bare — which means EAGER, the JPA default, and an N+1 waiting to happen. That was the single worst finding of this comparison against me, and it was one annotation. I fixed it, and it is live.

What I found in my own output

Comparing against someone else is mostly an excuse to read your own output as a stranger would. Two things fell out that had nothing to do with Bootify.

One-To-Many and Many-To-One were not producing the same product. Same domain, same data, but declaring the relation one way generated a <select> populated from the parent resource, and declaring it the other way generated a bare numeric input where the user was expected to type the parent's ID by hand — with no guard against 0. Both are legitimate JPA models and the frontend was faithfully following the backend in each case. It was still a trap: the wizard presents that choice as a matter of direction, and the "natural" reading of "a customer has many orders" led to the degraded form. Fixed — every foreign key now generates a dropdown.

Integration tests skip any entity with a mandatory foreign key. Still true as I write this. The reason is sound — POSTing an Order requires a Customer to already exist, and the fixture doesn't seed one — but the consequence is that on a realistic model, where most child entities have a required FK, integration test coverage lands on root entities only.
And it is silent: nothing in the generated README explains why OrderIntegrationTest isn't there. Seeding the parent is the fix. It is on the list.

What this comparison does not establish

It is one domain model, two entities, one relation. It says nothing about how either tool behaves on twenty entities, on inheritance, on many-to-many with payload, or on a schema that was designed by someone else five years ago. It does not measure generated code at runtime — I read it, I did not benchmark it.

And one caveat that cost me two wrong conclusions, in both directions: I should not have read either vendor's pages as a statement of what the generator emits — mine included. I recorded a competitor's capabilities from their documentation twice and was wrong both times, once against them and once in their favour. Everything stated as fact in this post came out of an unzipped project; anything I could only read on a page, I removed before publishing. Both generators emit Spring Boot 4.1.0 — that one I checked in the two build.gradle/pom.xml files, not on a website. If you are comparing generators, compare ZIPs.

What I took away

I expected to find that a competitor with a longer track record had a shallower output. The opposite was true on six counts, and one of them — the missing LAZY — was a genuine defect in what I was shipping to paying users.

What survived as a real difference is narrower than my marketing page implied, and I have since made the page say so: two separately deployable projects instead of one artifact, generated tests, and the deployment scaffolding around the code. That's it. That's the list.

If you generate Spring Boot projects, go read the ZIP your generator gives you. Read it like you would read a pull request from someone you don't know. I found two defects in my own product in an afternoon, and I wrote it.


I build SpringBoot Generator. Generating a project is free and needs no account, so you can check any claim in this post against the actual output. If you do read the generated code, I'd rather hear what's wrong with it than get a star — here in the comments, or at support@springboot-generator.com.

Top comments (0)