When I wrote my first real Java project, I had a folder of .java files, a folder of JAR files I had downloaded by hand and a very long javac command that I kept in a text file. When a teammate cloned the project, it didn't compile on their machine. Their JAR versions were different, their folders were different and the command had to be changed.
That's the problem Maven solves. You describe your project in one file, and one short command builds it the same way on every machine: it downloads the libraries, compiles the code, runs the tests and packages everything into a JAR or WAR.
In this post, we'll cover what build tools are, why we need them, how Maven works, what pom.xml is, the full archetype:generate command, Maven's terms (group id, artifact id, version, packaging), dependencies, goals, repositories and the three build lifecycles. I've drawn the important pictures in text so you can see how the pieces fit.
A note about versions: I'm using Maven 3.9.x, the stable line at the time of writing. Everything here works the same in newer versions.
Part 1: Build tools and why we need them
What is a build tool?
A build tool is a program that automates the steps between your source code and a finished, runnable application. Instead of running those steps by hand, you describe them once, and the tool repeats them exactly the same way every time.
The journey of your code
Every Java project goes through the same steps:
source code byte code unit tests package build output
(.java) --> (.class) --> (JUnit) --> (zip it up) --> .jar or .war
| | | |
you write javac check that deployable
compiles the code works format
-
Compile:
javacturns.javafiles into.classbytecode. - Test: unit tests check that your code behaves correctly.
-
Package: the
.classfiles and resources are bundled into one file. A JAR (Java ARchive) is for libraries and normal applications. A WAR (Web ARchive) is for web applications that run on a server like Tomcat.
Doing it by hand
Without a build tool, here's what a tiny project with one library needs:
javac -cp lib/gson-2.11.0.jar -d out src/com/example/App.java
java -cp out:lib/gson-2.11.0.jar com.example.App
jar cf app.jar -C out .
(On Windows, the classpath separator is ; instead of :.)
That's already awkward for one library and one file. Now imagine 200 files, 30 libraries, tests and three developers. The real problems are:
- Downloading libraries by hand. You have to find each JAR, pick a version and put it in a folder.
- Libraries need other libraries. Library A needs B, which needs C. You'd have to find all of them yourself.
- Version clashes. Two libraries need different versions of the same one. This is nicknamed "JAR hell."
- "Works on my machine." Everyone's folders and versions are slightly different.
- Long manual steps. Compile, test and package in the right order, every time.
Dependency management
A dependency is a library your project needs. Dependency management means the tool finds, downloads and organizes those libraries for you. You just say what you need, with a name and a version, and the tool does the rest, including the libraries that your libraries need.
WITHOUT a build tool WITH Maven
you --> search the web for gson.jar you --> write 5 lines in pom.xml
you --> download it, copy to lib/ |
you --> find the jars gson needs v
you --> fix the classpath by hand Maven --> downloads gson + what it needs
you --> repeat for every library and puts them on the classpath
The popular build tools
-
Ant: an early tool. You write every step yourself in
build.xml. Flexible, but a lot of manual work. -
Maven: you follow standard conventions and describe the project in
pom.xml. Dependencies are built in. This is what we're learning. - Gradle: similar goals to Maven, but build files are written in Groovy or Kotlin code instead of XML, and it's known for fast, incremental builds.
All three are used in real projects. Maven is the most common starting point, and its ideas (repositories, coordinates, lifecycles) show up everywhere.
Part 2: What is Apache Maven?
Apache Maven is a build automation and dependency management tool, mostly used for Java. Its name comes from a word meaning "accumulator of knowledge." The key idea is the POM (Project Object Model): a file called pom.xml that describes your project.
What does Maven do?
- Project structure. It gives every project the same standard folder layout.
- Dependencies. It downloads the libraries you list, and the ones they need.
- Unit tests. It runs your tests (for example, with JUnit) automatically.
- Compile. It compiles your code with the right Java settings.
- Package. It builds a deployable file (JAR or WAR).
And a few more: it can install the result into your local library folder, deploy it to a shared server, and generate project reports and documentation.
How does Maven do it?
Maven works on two simple ideas:
1. Convention over configuration. If you put files in the standard places and follow the standard naming, you don't have to configure anything. Maven already knows where your code and tests are, and where to put the output.
2. Plugins do the real work. Maven itself is a small "coordinator." It reads your pom.xml, decides which steps to run in which order, and hands each step to a plugin. The compiler plugin compiles, the surefire plugin runs tests, the jar plugin builds the JAR.
Here is what happens when you type one command:
mvn package
|
v
1. Maven reads pom.xml
|
v
2. Works out the lifecycle: validate -> compile -> test -> package
|
v
3. For each step, runs the plugin goal attached to it
compile -> compiler plugin (javac)
test -> surefire plugin (runs JUnit tests)
package -> jar plugin (builds the .jar)
|
v
4. Needs a library or plugin? Looks in the local repository,
and downloads it from the internet if it's missing
|
v
5. Puts all results in the "target" folder
Install and check
Maven needs a JDK, and the JAVA_HOME environment variable should point to it. After installing Maven, check it:
mvn -v
You'll see something like this (your numbers will differ):
Apache Maven 3.9.x
Maven home: /opt/maven
Java version: 17.0.x, vendor: ...
Look at the Java version line. Maven uses whichever JDK it finds, so make sure it's the one you intend.
The standard project structure
Every Maven project looks like this:
my-app/
|-- pom.xml <- the project description
|-- src/
| |-- main/
| | |-- java/ <- your application code (.java)
| | +-- resources/ <- config files, images, etc.
| +-- test/
| |-- java/ <- your unit tests
| +-- resources/ <- files used only by tests
+-- target/ <- created by Maven: all build output
-
src/mainis code that goes into your finished application. -
src/testis code that only checks it. It's never packaged. -
targetis created by Maven. It holds the compiled classes, test reports and the JAR. You can delete it any time and nothing is lost.
Because everyone uses this layout, you can open any Maven project in the world and find your way around.
pom.xml
The POM (Project Object Model) is the heart of a Maven project. Here's a small but complete example:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>my-app</name>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.0</version>
</plugin>
</plugins>
</build>
</project>
What each part means:
-
modelVersion: the version of the POM format. It's always4.0.0. -
groupId,artifactId,version: the project's identity (we'll explain them in a moment). -
packaging: what kind of file to build (jaris the default). -
name: a friendly name for humans. -
properties: settings and variables.maven.compiler.releaseis the Java version to compile for (set it to your JDK version, like 17 or 21), and the encoding avoids platform surprises. -
dependencies: the libraries you need. -
build/plugins: extra configuration for the build. Here we pin the test plugin version, which also makes sure JUnit 5 tests are found. Pinning plugin versions makes builds repeatable.
Behind the scenes: your POM is tiny
Your pom.xml looks short because it inherits from a hidden built-in file called the Super POM. It holds all the defaults: the folder layout, the repository address and the default plugins. Maven merges the Super POM and your POM into one effective POM. You can see the full result:
mvn help:effective-pom
It's long, but it shows you everything Maven is actually using.
Creating a project: mvn archetype:generate
You can create the folders and pom.xml by hand, but Maven can generate a ready-made project from a template. That's what archetype:generate does. Here's the full command:
mvn archetype:generate \
-DgroupId=com.example \
-DartifactId=my-app \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.5 \
-DinteractiveMode=false
(On Windows Command Prompt, write it on one line without the \ characters.)
Let's break it down piece by piece:
-
mvn: runs Maven. -
archetype:generate: this isplugin:goal. It means "use the archetype plugin and run its generate goal." -
-D: sets a property. Each option is written as-Dname=value. -
-DgroupId=com.example: the group id of your new project (usually your reversed domain or company). -
-DartifactId=my-app: the name of your project. Maven also creates a folder with this name. -
-DarchetypeArtifactId=maven-archetype-quickstart: which template to use. The quickstart template makes a simple Java project with a sample class and a sample test. -
-DarchetypeVersion=1.5: the version of that template. Version 1.5 generates a project that uses JUnit 5. (If your Maven can't find it, remove this option, and Maven will pick a version for you.) -
-DinteractiveMode=false: don't ask questions. Without it, Maven asks you for the template version, the package name and a final confirmation.
Two extra options you may see:
-
-Dversion=1.0-SNAPSHOT: the starting version (this is already the default). -
-Dpackage=com.example.app: the Java package for the sample classes. If you skip it, it defaults to the group id.
The first run downloads the archetype and some plugins from the internet, so it takes a little while. When it finishes, you'll see BUILD SUCCESS, and this project:
my-app/
|-- pom.xml
+-- src/
|-- main/java/com/example/App.java
+-- test/java/com/example/AppTest.java
The generated App.java is a simple "Hello World" with a main method. Open the generated pom.xml and check the Java version property. If it's older than your JDK, change maven.compiler.release to your Java version (for example 17 or 21).
Maven terminology
Archetype
An archetype is a project template. It's a pattern for creating new projects, like a "starter kit." Besides maven-archetype-quickstart, there are archetypes for web applications and more.
Maven coordinates: groupId, artifactId, version
Every project and library in the Maven world is identified by three values, called its coordinates (or GAV):
-
groupId: who it belongs to. Usually a reversed domain, like Java packages:com.example,org.junit.jupiter. -
artifactId: the name of the project or library:my-app,gson. -
version: which version:1.0-SNAPSHOT,2.11.0.
Together they point to exactly one thing, anywhere in the world:
com.google.code.gson : gson : 2.11.0
groupId artifactId version
The coordinates also decide where the file lives in a repository, which is just a folder path:
com.example : my-app : 1.0-SNAPSHOT
|
v dots in groupId become folders
com/example/my-app/1.0-SNAPSHOT/my-app-1.0-SNAPSHOT.jar
Version: SNAPSHOT vs release
- A version that ends in
-SNAPSHOT(like1.0-SNAPSHOT) means work in progress. It can change as you keep developing. Maven treats it as "may have a newer build," and can check for updates. - A version without
-SNAPSHOT(like1.0.0) is a release. It's a fixed, finished version. Once a release is published to a public repository such as Maven Central, it can never be changed or replaced.
A typical life of a version: 1.0.0-SNAPSHOT while you build it, then you publish 1.0.0 (the release), then you start 1.1.0-SNAPSHOT for the next work. A good tip is to follow a clear numbering scheme, such as major.minor.patch.
Packaging type
packaging tells Maven what to build from your project. It also decides which plugin goals run at each step of the lifecycle.
-
jar(the default): a library or a normal application. -
war: a web application for a server like Tomcat. -
pom: no code of its own. Used for parent projects that group other projects. -
ear: an enterprise application (less common today).
Maven dependencies
A dependency is declared with its coordinates:
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
</xml>
Save the file, and Maven downloads the library the next time you build. Now you can use it in your code right away:
import com.google.gson.Gson;
import java.util.Map;
public class App {
public static void main(String[] args) {
Gson gson = new Gson();
System.out.println(gson.toJson(Map.of("name", "Asha")));
}
}
Output:
{"name":"Asha"}
No JAR downloads and no classpath editing.
Scope: when is the dependency needed?
The <scope> tag says when a library is needed:
-
compile(default): needed to compile and run. It's packaged with your app. -
test: only for compiling and running tests, like JUnit. Not packaged. -
provided: needed to compile, but the server provides it when running (for example, the servlet API on Tomcat). Not packaged. -
runtime: not needed to compile, but needed when running (for example, a database driver).
Using the right scope keeps your final package small and clean.
Transitive dependencies
Libraries often depend on other libraries. If you add library A, and A needs B, Maven fetches B too. These are called transitive dependencies. You can see the whole tree:
mvn dependency:tree
[INFO] com.example:my-app:jar:1.0-SNAPSHOT
[INFO] +- com.google.code.gson:gson:jar:2.11.0:compile
[INFO] \- org.junit.jupiter:junit-jupiter:jar:5.11.0:test
[INFO] +- org.junit.jupiter:junit-jupiter-api:jar:5.11.0:test
[INFO] ...
(The list is trimmed here. Maven also shows the libraries that each one needs, indented underneath.)
What if two libraries need different versions of the same third library? Maven uses a simple rule: the version nearest to your project wins. If they're equally near, the first one declared wins.
my-app
|-- lib-a 1.0
| +-- lib-b 2.0 (two steps away)
+-- lib-c 1.0
+-- lib-b 1.5 (two steps away: a tie, so the first one, 2.0, wins)
To take control, declare lib-b yourself in your pom.xml. A direct dependency is nearest.
Maven goals and phases
Here's an important distinction:
- A goal is one specific task done by a plugin. It's written as
plugin:goal, likecompiler:compileorsurefire:test. - A phase is a named step in a lifecycle, like
compileortest. Each phase has goals attached to it.
When you run a phase, Maven runs every earlier phase first, then the goals attached to that one. You can also run a goal directly, like mvn archetype:generate.
The commands you'll use most:
-
mvn clean: deletes thetargetfolder, so you start fresh. -
mvn compile: compiles the main code. -
mvn test: compiles, then runs the unit tests. -
mvn package: runs the tests, then builds the JAR (or WAR). -
mvn install: packages, then copies the result into your local repository, so other projects on your machine can use it. -
mvn deploy: uploads the package to a remote repository for others to use.
Here is what a run looks like. This is a trimmed mvn package:
[INFO] --- resources:3.x:resources (default-resources) @ my-app ---
[INFO] --- compiler:3.x:compile (default-compile) @ my-app ---
[INFO] Compiling 1 source file with javac [debug release 17] to target/classes
[INFO] --- surefire:3.5.0:test (default-test) @ my-app ---
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- jar:3.x:jar (default-jar) @ my-app ---
[INFO] Building jar: /.../my-app/target/my-app-1.0-SNAPSHOT.jar
[INFO] BUILD SUCCESS
(Version numbers on your screen will differ.) Each --- plugin:version:goal --- line is one goal running. You can see Maven working through the lifecycle: resources, compile, test, jar.
Testing with JUnit
Let's write a real class and test. Create src/main/java/com/example/Calculator.java:
package com.example;
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
And the test, src/test/java/com/example/CalculatorTest.java:
package com.example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class CalculatorTest {
@Test
void addsTwoNumbers() {
assertEquals(5, new Calculator().add(2, 3));
}
}
mvn test
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
Now break the test on purpose, by changing 5 to 6, and run mvn package:
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
[INFO] BUILD FAILURE
Look what didn't happen: no JAR was created. The test phase failed, so Maven stopped before the package phase. That's the value of the lifecycle: broken code can't become a package by accident.
Maven's test plugin finds tests by name. By default it looks for classes named *Test, Test*, *Tests or *TestCase, in src/test/java.
Running your JAR
After mvn package, your JAR is in target/. If you try to run it like a normal application:
java -jar target/my-app-1.0-SNAPSHOT.jar
no main manifest attribute, in target/my-app-1.0-SNAPSHOT.jar
The JAR doesn't say which class has main. You can run it by naming the class:
java -cp target/my-app-1.0-SNAPSHOT.jar com.example.App
Or make the JAR runnable by telling the jar plugin about your main class, in pom.xml:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<mainClass>com.example.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
(If your app uses outside libraries, you'd need another plugin, such as the shade or assembly plugin, to bundle them into one "fat" JAR.)
Maven repositories
A repository is a storage place for libraries and plugins, organized by their coordinates. Maven uses three kinds:
-
Local repository: a folder on your own computer, by default
~/.m2/repository. It's a cache of everything you've downloaded, plus anything youinstall. - Central repository: Maven Central, the huge public repository on the internet. It's where most open-source libraries live, and Maven uses it by default.
- Remote repository: any other server. Companies often run their own (with tools such as Nexus or Artifactory) for private libraries, and to cache Central.
When Maven needs a library, it follows this search:
Need gson 2.11.0
|
v
1. Local repository (~/.m2/repository) --- found? ---> use it (no internet needed)
| not found
v
2. Remote repositories you configured (company server) --- found? ---+
| not found |
v v
3. Maven Central (internet) --- found? ---> download ---------> save a copy in
the local repository
Because of that last step, each library is downloaded only once. Later builds use the local copy, and you can even work offline with mvn -o.
How the lifecycle phases connect to repositories:
-
installcopies your JAR into the local repository. -
deployuploads it to a remote repository (you must configure the server address and your login).
You can configure repositories, mirrors and passwords in a settings.xml file in your .m2 folder.
The Maven build lifecycle
A build lifecycle is a fixed sequence of phases. Maven has three built-in lifecycles: default, clean and site.
1. The default lifecycle (building your project)
These are the main phases, in order:
validate --> compile --> test --> package --> verify --> install --> deploy
-
validate: checks that the project is correct and the needed information is there. -
compile: compiles the source code insrc/main/java. -
test: runs unit tests, using the compiled code. The tests don't need to be packaged. -
package: bundles the compiled code into the packaging type (JAR, WAR). -
verify: runs any extra checks on the package, like integration tests and quality rules. -
install: copies the package into the local repository. -
deploy: copies the final package to a remote repository for sharing.
(The real lifecycle has more phases in between, such as generate-sources, process-resources and test-compile. The ones above are the ones you'll use directly.)
The key rule: when you run a phase, Maven runs all the phases before it, in order.
mvn compile runs: validate, compile
mvn test runs: validate, compile, test
mvn package runs: validate, compile, test, package
mvn install runs: validate, compile, test, package, verify, install
You never need to run mvn compile and then mvn test separately. Just run the last phase you want.
What runs in each phase?
Each phase has plugin goals attached, and which ones depend on your packaging type. For a normal jar project:
PHASE GOAL THAT RUNS WHAT IT DOES
compile compiler:compile javac on src/main/java
test surefire:test runs the unit tests
package jar:jar builds the .jar
install install:install copies it to ~/.m2
deploy deploy:deploy uploads to a remote repository
For packaging "war": package runs war:war instead of jar:jar
This answers the question "how does Maven know what to do?" The packaging type chooses which goals are attached to which phases. That's why <packaging> matters.
2. The clean lifecycle
This one cleans up. It has three phases: pre-clean, clean and post-clean. In practice you use only clean, which deletes the target folder.
mvn clean
It's the way to get rid of old compiled files, so nothing stale is left behind. You'll often combine it with the default lifecycle:
mvn clean package
Maven runs clean (the clean lifecycle) first, then the default lifecycle up to package.
3. The site lifecycle
This one creates project documentation and reports. Its phases are pre-site, site, post-site and site-deploy.
mvn site
It generates a website describing your project (test reports, dependency lists, and so on) in target/site. It's optional, and less common in day-to-day work than the other two.
What's inside target/ after a build?
target/
|-- classes/ <- compiled main code (.class)
|-- test-classes/ <- compiled test code
|-- surefire-reports/ <- test result reports
+-- my-app-1.0-SNAPSHOT.jar <- the packaged result
A few tips and commands
-
Skip tests when you must (for example, to package quickly):
mvn package -DskipTests. This still compiles the tests.-Dmaven.test.skip=trueskips compiling them too. Don't make this a habit, since tests are your safety net. -
Run one test class:
mvn test -Dtest=CalculatorTest -
Force re-download of snapshots and failed items:
mvn package -U -
Work offline:
mvn -o package -
See the dependency tree:
mvn dependency:tree -
See the full merged POM:
mvn help:effective-pom
Which one should I use?
-
mvn package,verifyorinstall? Usepackageto build the JAR. Useverifywhen you also want the extra checks. Useinstallwhen another project on your machine needs this one as a dependency. -
mvn clean packageor justpackage? Usecleanwhen you want a guaranteed fresh build, for example after renaming or deleting files, so old.classfiles don't linger. - JAR or WAR? A JAR for libraries and normal applications. A WAR for web apps that go on a server like Tomcat. (Frameworks such as Spring Boot also build runnable JARs with an embedded server.)
-
SNAPSHOT or release? Use
-SNAPSHOTwhile developing. Publish a release version when something is finished and tagged, because releases never change. -
Which scope?
testfor testing libraries,providedfor things the server gives you,runtimefor things only needed when running, andcompilefor everything else. - Archetype or generator website? For plain Java, an archetype is great. For Spring Boot projects, most people use the Spring Initializr website, which creates a ready Maven project.
- Maven or Gradle? Both work well. Maven is more standard and predictable. Gradle is more flexible and often faster on big builds. Learning Maven first gives you the concepts that both share.
Common mistakes
-
JAVA_HOMEor Java version problems. Ifmvn -vshows the wrong Java, or fails, fixJAVA_HOME. Also setmaven.compiler.releaseinpom.xmlto match your JDK. -
Running
java -jaron a JAR with no main class. You get "no main manifest attribute." Set the main class in the jar plugin, or run it with-cpand the class name. -
Wrong folders or test names. Code outside
src/main/java, or tests outsidesrc/test/javaor not named like*Test, are silently ignored. "No tests to run" usually means this. -
Skipping tests all the time.
-DskipTestshides real problems. Use it rarely. -
Version surprises. Using
SNAPSHOTdependencies in something you release, or two libraries needing different versions of a third. Check withmvn dependency:tree, and declare the version you want directly. -
Stale or broken builds. Old classes in
target/or a half-downloaded file in~/.m2can cause strange errors. Runmvn clean, and use-Uto force a fresh download.
Quick recap
- A build tool automates compile, test, package and dependency handling, so builds are repeatable on every machine.
- Dependency management means you declare what you need, and the tool downloads it, including what your libraries need.
- The code journey:
.java→.class→ unit tests → package →.jar/.war. -
Maven follows convention over configuration. It reads
pom.xml, runs the lifecycle, and uses plugins to do each step. - The standard layout:
src/main/java,src/main/resources,src/test/java, and output intarget/. -
pom.xmlholds the project's identity (groupId,artifactId,version,packaging), properties, dependencies and plugins. It inherits defaults from the Super POM. -
mvn archetype:generatecreates a project from a template.-Doptions set properties. -
Coordinates (
groupId:artifactId:version) identify anything uniquely.-SNAPSHOTmeans in development, and a release never changes. -
Packaging (
jar,war,pom) decides what is built. -
Goals are plugin tasks (
plugin:goal). Phases are lifecycle steps. Running a phase runs all earlier ones. -
Repositories: local (
~/.m2), central (Maven Central) and remote (company servers).installgoes to local,deploygoes to remote. -
Lifecycles: default (
validate → compile → test → package → verify → install → deploy), clean (deletestarget) and site (generates documentation).
Try it yourself
Here's a small practice idea: generate a project with the archetype:generate command from this post. Add the Calculator class and CalculatorTest, run mvn test, and then break the test on purpose and run mvn package to see that no JAR gets built. Fix it, run mvn clean package, and find your JAR in target/. Finally, run mvn dependency:tree and mvn help:effective-pom and see how much Maven was doing for you.
For a bonus experiment, add the Gson dependency, print some JSON from your App, and then look inside ~/.m2/repository/com/google/code/gson to see the exact folder path your coordinates turned into.
If this helped, or if you spotted something I should fix, tell me in the comments. I'm still learning too.
Top comments (0)