1. Reproducible repository
When you are just starting out, it seems that “the project is ready” means “the green Run button works in IDEA on my machine”. That is a normal stage; we all went through it, only some people stayed there for ten years. But in the backend world the criterion is different: a repository is considered alive if it can be cloned onto a clean machine and launched with the commands that are written directly in README.md.
The Wrapper, fixed versions, and canonical launch commands are already there. Now all that remains is to organize everything so that the project lives properly in the repository and does not depend on your IDE.
Let us mentally imagine your future self — tired — or a colleague who is even more tired and opens the repository. They do not know your Gradle version, your IDE settings, or which “Run Configurations” you clicked together. They only see files and text. And this is where README and the project root start playing the role of a kind voice in the dark: they either get a person to a working launch in two minutes, or they turn the start into a quest called “find the right checkbox”.
It is useful to keep a simple scheme in mind:
flowchart TD
%% Check the baseline launch chain: from clone to run
A["Cloned the repository"] --> B["Opened README.md"]
B --> C["Ran the commands through ./gradlew"]
C --> D["The project built: clean build"]
D --> E["The project launched: run"]
If this chain breaks somewhere in the middle, the repository is still not ready for reproducible launch. Not a tragedy, but also not “done”.
2. The project root as an “entryway”
The project root directory is like an entryway in an apartment. Sure, you can store a bicycle, boxes, garlands, and three giant teddy bears there, but for a guest — that is, anyone who opens the repository — it will be hard to understand where to go at all. In a good Gradle project, the root contains only a few things, but each one is obvious and in its place.
For ReadLater Starter at this stage, this structure is important to us (roughly):
ReadLater Starter/
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
├── build.gradle.kts
├── README.md
├── .gitignore
└── src/
└── main/
├── java/
└── resources/
The secret is that this is not “beauty for beauty’s sake”. It is a set of files that answers the questions: “how do I build?”, “how do I run?”, “what Gradle version?”, “where is the code?”, “where are the resources?” It is especially important that the Wrapper files are in the repository; otherwise, the idea of reproducibility turns into “well, install Gradle, find the version, try...”.
Let us summarize one last time in a table: “what is responsible for what”:
| Element | What it is needed for in practice |
|---|---|
gradlew, |
The single entry point for building on Linux/macOS and Windows |
| gradle/wrapper/* | Locking the Gradle version and the ability to download the required Gradle automatically |
| settings.gradle.kts | Project name and basic build configuration (in our course — without multi-module) |
| build.gradle.kts | Plugins, dependencies, application setup, and build rules |
| src/main/java | Your Java code |
| src/main/resources | Configs and resources that should go onto the classpath |
| README.md | Instruction “what this is and how to launch it” |
| .gitignore | Protecting the repository from junk (build/, .idea/, etc.), but not from the Wrapper |
3. README.md as a contract
Many people treat a README for a learning project as “I’ll add it later”. That is a trap. In reality, README is not decoration, but part of the baseline: it records the agreement about how to launch. If you forget how to run the project a week from now, the README should become the document that saves you from archaeological digs in terminal history.
A good README at this stage does not have to be a novel. It only needs to answer three questions honestly: what you need to have, what commands to build with, and what commands to launch with. And there is a fourth one, especially important for a beginner: “what should I see if everything is OK?” Because “nothing blew up” is also a result, but you want confirmation that the right thing launched.
Let us sketch out a minimal but useful README scaffold. Do not worry, this is not “bureaucratese”, but an instruction for another person — who, oddly enough, is almost always you.
# ReadLater Starter
A course project for the "Java Server" course.
## Requirements
- JDK 25
## Build
./gradlew clean build
## Run
./gradlew run
Yes, it is short. But it is already better than “the README will come later”. And most importantly: all commands here go through ./gradlew, not through “install Gradle”.
At this stage, we fix Java 25 exactly this way: as an environment requirement in the README and through checking ./gradlew --version. That is enough for the baseline to be honest and clear; we are not going into Gradle toolchain configuration right now.
If you want to make the README even more useful without bloating it, add a “Environment check” section and a short block with typical problems. For example:
## Environment check
./gradlew --version
This command often saves you when it suddenly turns out that the project runs on a different JVM than you expected.
4. Commands in the README must work
A README becomes truly valuable only in one case: if the commands in it can be copied and pasted without adjustments. That is why after any change related to building or launching, we simply run the familiar minimum from the project root:
./gradlew --version
./gradlew clean build
./gradlew run
On Windows, the same commands are run through gradlew.bat.
There is no need to re-explain the mechanics of each command here. The point of the check is different: the README, build.gradle.kts, and the actual state of the project must not diverge. If the instructions promise one entry point, but the project lives by a different scenario, the baseline falls apart again.
5. .gitignore and the Wrapper
At this stage, many people encounter an unexpected problem for the first time: Gradle creates its own folders and files, the IDE creates its own, and you want only what actually belongs to the project to end up in git. That is where .gitignore is needed. But there is a tricky point: a beginner can accidentally “throw out” exactly the thing that ensures reproducibility from the repository.
The gradle/ folder and Wrapper files suffer the most. From a distance it seems: “well, those are some Gradle files, probably they are not needed.” And then the repository gets cloned, and the commands ./gradlew ... stop working because the Wrapper simply disappeared. So the rule is simple: ignore .gradle/ (cache), but do not ignore gradle/ (wrapper).
A minimal .gitignore fragment that is often good enough at the start:
# Gradle build output
/build/
# Gradle cache can safely be left out of git
/.gradle/
# IMPORTANT: do not ignore the gradle/ folder — that is where the wrapper lives
# IDE
/.idea/
/out/
*.iml
# OS
.DS_Store
Notice the subtle point: /build/ and /.gradle/ — yes, gradle/ — no.
Another useful principle for a clean repository: if you are unsure whether to ignore a file, ask yourself: “Can this file be restored from the repository automatically?” build/ can be restored with ./gradlew build, so it is not needed in git. But gradle-wrapper.properties and gradle-wrapper.jar cannot be restored without the repository, so they must be in git.
6. Smoke check: clean build
Sometimes a project works “overall”, but the baseline is still fragile. For example, you accidentally put a resource in the wrong place, the build only passes after launching from the IDE, or run works only after you have already run build once. To avoid catching these surprises later, at the worst possible moment, it is useful to do a small smoke check.
The commands are already fixed; now it is important to understand exactly what they confirm. It is convenient to keep the following short check in mind:
| What we want to guarantee | What counts as success |
|---|---|
| We are running the Wrapper itself | In the output of --version we see Gradle 9.4.0 and JVM 25 |
| The build is reproducible | clean build passes without IDE involvement |
| The launch is reproducible | run gives a clear sign of life |
| The README does not lie | The commands from the README can be copied without changes |
The point of a smoke check is not to “catch all bugs”. It is to ensure that the project is not held hostage by your IDE. When this set passes without surprises, the ground under the build becomes solid. And this is an important moment: after that you can switch to HTTP and request/response without confusing transport-level problems with build problems.
7. ReadLaterApplication: a sign of life
For a smoke check, you do not need another separate code example. It is enough for the current com.example.readlater.ReadLaterApplication to give a short and recognizable sign of life in the logs when running ./gradlew run. The point is not the beauty of the startup message, but that after cloning the repository you can immediately tell “the application really started” from “Gradle just exited quietly”.
For example, the console might show something like:
INFO ReadLater Starter started
INFO Args count = 0
The line format does not matter yet; what matters is something else: the entrypoint is alive, logging is connected, and launching through the Wrapper gives the expected result.
8. Typical mistakes when preparing a repository
Even at such a “simple” step as the README and repository structure, beginners regularly step on the same rakes. The most unpleasant part is that these rakes are often invisible until you try to do exactly what any other person should do: clone the repository and run the commands. So below is a set of mistakes that is useful to learn to recognize by symptoms.
Mistake #1: the README says “gradle build”, but the project requires the Wrapper.
This looks harmless: you write “gradle build” because Gradle is installed on your machine. But the point of the course and the project baseline is that the command must work for someone who does not have Gradle installed at all. As soon as you write “gradle ...”, you turn reproducibility into a lottery: one person has Gradle 9.4.0, another has 8.x, and a third has whatever was found. The remedy is simple: the README should contain commands only through ./gradlew or gradlew.bat.
Mistake #2: .gitignore accidentally ignores gradle/ and Wrapper files.
The most classic scenario: “I saw a .gitignore for Gradle somewhere, copied it” — and suddenly gradle/wrapper/gradle-wrapper.jar did not make it into the repository. Locally everything works because the files are already there, but for another person — no. Symptom: after cloning, the command ./gradlew fails almost immediately. Check your .gitignore: you need to ignore /.gradle/, but not gradle/.
Mistake #3: the commands in the README do not match the actual state of the project.
README becomes outdated very easily. You renamed the main class, changed the package, and the README still suggests ./gradlew run. As a result, the new person gets the error “Main class not found” and thinks the project is broken. In reality, the contract is broken. The only working approach is to run the commands from the README after any change that affects launching and make sure they are still current.
Mistake #4: the project launches from the IDE, but does not launch through ./gradlew run.
This is the same “IDE magic” we are escaping from. Sometimes the IDE picks up the classpath, resources, and launch parameters in such a way that the project “kind of works”. And then you run ./gradlew run and get a different result. That is a useful signal: the baseline is still not aligned. Often the reason is that application { mainClass = "..." } was not configured, or the main class moved and Gradle does not know about it.
Mistake #5: you forgot to state the environment requirements explicitly (JDK 25), and the project builds “on whatever happens to be there”.
If the README says nothing about the JDK, a student might try to build the project on JDK 17 and then be surprised by compatibility errors. Even if you do not use Gradle toolchains right now, the minimal honesty is to write in the README “JDK 25 is required” and suggest checking it with ./gradlew --version. That saves dozens of minutes of “why is it failing?”.
GO TO FULL VERSION