Build a Discord Bot
Estimated time: 4–5 hours | Difficulty: Intermediate
What You Will Learn
- Understand what Discord bots are and how they use Discord's API
- Set up a Discord application and bot user at discord.com/developers
- Build a Java bot using JDA (Java Discord API)
- Listen for messages and respond to commands
- Create slash commands with formatted embed responses
- Build useful bot features:
/help,/remind,/poll - Connect your bot to a MySQL database and external APIs
- Understand deployment considerations for always-on bots
1. What Is a Discord Bot?
If you have spent any time on Discord, you have seen bots. They are the users with the little "BOT" tag next to their name. They greet new members, play music, moderate chat, run polls, look up information, and do a thousand other things. They feel like magic. But they are not magic. They are just programs — programs that connect to Discord's servers, listen for events, and respond.
Here is the part that should excite you: you already know how to build one.
Think about what you built with Resumator. You wrote a Spring Boot application that listens for HTTP requests, processes them, and sends back responses. A Discord bot does exactly the same thing, except instead of listening for HTTP requests from a browser, it listens for events from Discord — messages sent in a channel, a user joining a server, someone typing a slash command. Instead of sending back HTML or JSON, it sends back messages, embeds, and reactions.
The mental model is the same:
- REST API: Browser sends HTTP request → Your controller handles it → You send back a response
- Discord Bot: User sends a message or command → Your listener handles it → You send back a reply
Same pattern. Different interface. If you can build one, you can build the other.
We will use a library called JDA — Java Discord API. JDA is to Discord what Spring Boot is to web development: it handles all the messy low-level details (WebSocket connections, rate limiting, authentication) and gives you clean, simple Java methods to work with. You register listeners, and JDA calls your code when things happen. You have seen this pattern before. It is event-driven programming, and it is everywhere in software.
By the end of this side quest, you will have a real bot running in a real Discord server that your friends can actually use. Let us get started.
2. Setting Up Your Bot
Before you write a single line of Java, you need to do some setup on Discord's side. You are going to create a Discord Application, attach a Bot user to it, get your bot token, and invite it to a test server. This is the equivalent of setting up your database and API keys before building Resumator.
Step 1: Create a Discord Application
Go to discord.com/developers/applications and sign in with your Discord account. Click "New Application" in the top-right corner. Give it a name — something like "Coders Farm Bot" or whatever you want to call it. Click Create.
You are now looking at your application's dashboard. This is where Discord keeps all the settings for your bot. Think of the "Application" as the container, and the "Bot" as the actual user that will appear in servers.
Step 2: Create the Bot User
In the left sidebar, click "Bot". Then click "Add Bot" and confirm. Discord will create a bot user attached to your application. You will see options for its username and profile picture — you can customize these later.
Now find the Token section and click "Reset Token" (or "Copy" if it is visible). Copy this token and save it somewhere safe.
Security: Your Bot Token Is a Password
Your bot token is exactly like an API key or a database password. Anyone who has it can control your bot — send messages as it, read channels it has access to, anything. Never commit your token to Git. Never paste it in a public channel. Never hardcode it in your Java files.
You will store it in application.properties (which should be in your .gitignore), just like you stored your JSearch API key for Resumator. Same principle, same solution. If your token ever leaks, go back to the Developer Portal and reset it immediately.
Step 3: Set Permissions and Invite the Bot
In the left sidebar, click "OAuth2". Scroll down to "OAuth2 URL Generator". Under Scopes, check bot and applications.commands. Under Bot Permissions, check:
- Send Messages
- Read Message History
- Use Slash Commands
- Embed Links
- Add Reactions
- Manage Messages (for the poll feature later)
Discord will generate an invite URL at the bottom. Copy it, paste it into your browser, and select the test server you want to invite the bot to. If you do not have a test server yet, create one — it takes five seconds. Click Authorize, and your bot will appear in the server's member list (it will be offline until you run your code).
Step 4: Create Your Java Project
Create a new Java project with Maven, just like you have done before. You can use IntelliJ's "New Project" wizard or Spring Initializr. For this project, you do not need Spring Boot — JDA is a standalone library. But if you want to add database or API features later (we will), starting with Spring Boot makes that easier.
The critical step is adding JDA to your pom.xml. Add this dependency:
<!-- In your pom.xml, inside the <dependencies> section -->
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>5.1.2</version>
</dependency>
If Maven does not find JDA in Maven Central, you may need to add the JDA repository. Check the JDA GitHub page for the latest version and instructions. Once Maven downloads the dependency, you are ready to write code.
Now set up your token. In src/main/resources/application.properties:
# Discord bot token - NEVER commit this file to Git
discord.bot.token=YOUR_TOKEN_HERE
And make sure your .gitignore includes application.properties (or at least the line with your token). You have done this before with API keys. Same discipline applies here.
3. Your First Bot
This is the moment. You are going to write a few lines of Java, run the program, and watch your bot come to life in Discord. This is the "Hello World" of bot development, and it is one of the most satisfying moments you will have as a programmer — because you will see your code appear as a real user in a real application that other people use.
Create a class called DiscordBot.java. This is your main class — the entry point for your bot:
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.requests.GatewayIntent;
import java.io.InputStream;
import java.util.Properties;
public class DiscordBot {
public static void main(String[] args) throws Exception {
// Load the token from application.properties
Properties props = new Properties();
InputStream input = DiscordBot.class.getClassLoader()
.getResourceAsStream("application.properties");
props.load(input);
String token = props.getProperty("discord.bot.token");
// Build the JDA instance and connect to Discord
JDA jda = JDABuilder.createDefault(token)
.setActivity(Activity.playing("with Java code"))
.enableIntents(GatewayIntent.MESSAGE_CONTENT)
.addEventListeners(new MessageListener())
.build();
// Wait until the bot is fully connected
jda.awaitReady();
System.out.println("Bot is online! Connected as: "
+ jda.getSelfUser().getName());
}
}
Let us break this down piece by piece, because every line matters:
JDABuilder.createDefault(token)— This creates a builder for your bot connection, using your token to authenticate. Think of it likeDriverManager.getConnection()for databases, but for Discord..setActivity(Activity.playing("with Java code"))— This sets the "Playing..." status you see under a user's name in the member list. It is cosmetic but fun..enableIntents(GatewayIntent.MESSAGE_CONTENT)— Discord requires you to explicitly opt in to receiving message content. This is a privacy feature. Without it, your bot can see that a message was sent, but not what it says..addEventListeners(new MessageListener())— This registers a listener class that will handle events. We will write this next..build()— This actually connects to Discord. Behind the scenes, JDA opens a WebSocket connection to Discord's gateway server.jda.awaitReady()— This blocks until the bot is fully connected and has received all the initial data (server list, channel list, member lists, etc.).
Now create the MessageListener class. This is where you handle events:
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.session.ReadyEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
public class MessageListener extends ListenerAdapter {
@Override
public void onReady(ReadyEvent event) {
System.out.println("Bot is ready and listening for events!");
}
@Override
public void onMessageReceived(MessageReceivedEvent event) {
// Ignore messages from bots (including ourselves!)
if (event.getAuthor().isBot()) return;
String message = event.getMessage().getContentRaw();
if (message.equalsIgnoreCase("!hello")) {
event.getChannel().sendMessage(
"Hello, " + event.getAuthor().getName()
+ "! I am a bot built with Java and JDA."
).queue();
}
if (message.equalsIgnoreCase("!ping")) {
long time = System.currentTimeMillis();
event.getChannel().sendMessage("Pong!")
.queue(response -> {
response.editMessageFormat("Pong! Latency: %dms",
System.currentTimeMillis() - time).queue();
});
}
}
}
Look at this class carefully. ListenerAdapter is a JDA class that provides empty default implementations for every possible Discord event. You override only the ones you care about. When Discord sends an event, JDA routes it to the appropriate method. You do not need to parse raw WebSocket frames or deal with JSON — JDA gives you clean Java objects like MessageReceivedEvent that have methods like getAuthor() and getChannel().
Notice the .queue() call at the end of sendMessage(). This is important. JDA sends messages asynchronously by default. .queue() means "send this in the background and do not block." You could use .complete() to send synchronously, but .queue() is the right choice for almost everything because it keeps your bot responsive. The !ping command even uses the callback version of .queue() to measure round-trip latency.
Also notice the first line of onMessageReceived: we check if the message author is a bot and return early if so. This is critical. Without this check, your bot could respond to its own messages, which could trigger it to respond again, creating an infinite loop. Always ignore bot messages.
Run the program. Open IntelliJ, click Run, and watch the console. You should see "Bot is online!" and "Bot is ready and listening for events!" Switch to Discord, find your test server, and type !hello in any channel. Your bot will respond. It is alive.
Take a moment to appreciate this. You wrote Java code on your machine, and it is controlling a user in a real, live application. This is what programming feels like when it gets fun.
Event-Driven Programming
What you just built uses the event-driven programming model. Your bot does not constantly check "did anyone send a message? how about now? now?" Instead, it registers listeners and waits. When something happens, the system calls your code. This is the same model used by JavaScript in the browser (click handlers, event listeners), by Android apps (button tap handlers), and by most real-time systems. The code you wrote for MessageListener is no different from a Spring @Controller method — both sit idle until they are called by the framework when a relevant event occurs.
4. Slash Commands
The !hello command you just built is called a prefix command — it uses a prefix character (!) to indicate that a message is a command. Prefix commands work, but Discord has a much better system now: slash commands.
When a user types / in Discord, a menu pops up showing all available commands from all bots in the server. Each command has a description, and Discord handles argument parsing, validation, and even autocomplete. It is a dramatically better user experience, and it is the standard that Discord expects bots to use.
To use slash commands, you need to do two things: register the commands with Discord (tell Discord what commands your bot supports) and handle them when users invoke them.
Let us update your DiscordBot.java main class to register commands on startup:
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.requests.GatewayIntent;
import java.io.InputStream;
import java.util.Properties;
public class DiscordBot {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
InputStream input = DiscordBot.class.getClassLoader()
.getResourceAsStream("application.properties");
props.load(input);
String token = props.getProperty("discord.bot.token");
JDA jda = JDABuilder.createDefault(token)
.setActivity(Activity.playing("with Java code"))
.enableIntents(GatewayIntent.MESSAGE_CONTENT)
.addEventListeners(new MessageListener())
.addEventListeners(new SlashCommandListener())
.build();
jda.awaitReady();
// Register slash commands with Discord
jda.updateCommands().addCommands(
Commands.slash("greet", "Get a friendly greeting from the bot"),
Commands.slash("roll", "Roll a dice")
.addOption(OptionType.INTEGER, "sides",
"Number of sides on the dice (default: 6)", false),
Commands.slash("quote", "Get a random inspirational quote")
).queue();
System.out.println("Bot is online with slash commands!");
}
}
The updateCommands() method tells Discord about the commands your bot supports. Each command has a name and a description. The /roll command also has an option — an optional integer parameter for the number of sides. Discord will show all of this in its slash command menu, and it will validate types for you. If a user tries to pass text where you expect a number, Discord will stop them before your code even runs.
Now create the SlashCommandListener class to handle these commands:
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import java.awt.Color;
import java.util.List;
import java.util.Random;
public class SlashCommandListener extends ListenerAdapter {
private final Random random = new Random();
private final List<String> quotes = List.of(
"The best way to predict the future is to create it. — Peter Drucker",
"Code is like humor. When you have to explain it, it is bad. — Cory House",
"First, solve the problem. Then, write the code. — John Johnson",
"The only way to learn a new programming language is by writing programs in it. — Dennis Ritchie",
"Simplicity is the soul of efficiency. — Austin Freeman",
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand. — Martin Fowler"
);
@Override
public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
switch (event.getName()) {
case "greet" -> handleGreet(event);
case "roll" -> handleRoll(event);
case "quote" -> handleQuote(event);
}
}
private void handleGreet(SlashCommandInteractionEvent event) {
EmbedBuilder embed = new EmbedBuilder()
.setTitle("Hello, " + event.getUser().getName() + "!")
.setDescription("Welcome! I am a bot built with Java and JDA "
+ "as part of the Coders Farm curriculum.")
.setColor(Color.decode("#5865F2"))
.setThumbnail(event.getUser().getAvatarUrl())
.addField("Your ID", event.getUser().getId(), true)
.addField("Server", event.getGuild().getName(), true)
.setFooter("Built with JDA and Java");
event.replyEmbeds(embed.build()).queue();
}
private void handleRoll(SlashCommandInteractionEvent event) {
int sides = event.getOption("sides") != null
? event.getOption("sides").getAsInt()
: 6;
if (sides < 2) {
event.reply("A dice needs at least 2 sides!").setEphemeral(true).queue();
return;
}
int result = random.nextInt(sides) + 1;
EmbedBuilder embed = new EmbedBuilder()
.setTitle("Dice Roll")
.setDescription("Rolling a **d" + sides + "**...")
.addField("Result", "**" + result + "**", false)
.setColor(result == sides ? Color.YELLOW : Color.BLUE)
.setFooter("Rolled by " + event.getUser().getName());
event.replyEmbeds(embed.build()).queue();
}
private void handleQuote(SlashCommandInteractionEvent event) {
String quote = quotes.get(random.nextInt(quotes.size()));
EmbedBuilder embed = new EmbedBuilder()
.setTitle("Inspirational Quote")
.setDescription("*" + quote + "*")
.setColor(Color.decode("#2ECC71"))
.setFooter("Requested by " + event.getUser().getName());
event.replyEmbeds(embed.build()).queue();
}
}
There is a lot to unpack here, so let us go through it.
The onSlashCommandInteraction method is called by JDA whenever a user invokes a slash command. It receives a SlashCommandInteractionEvent that contains everything you need: which command was used, who used it, what options they provided, and methods to reply. We use a switch expression (the modern Java syntax with arrows) to route to the correct handler method.
Embeds are rich formatted messages. Instead of plain text, embeds have titles, descriptions, fields, colors, thumbnails, and footers. They look professional and are much easier to read than plain text for structured information. The EmbedBuilder class uses the builder pattern you have seen before — chain method calls to construct the object, then call .build() at the end.
The /roll command demonstrates option handling. We check if the user provided the "sides" option. If they did, we use their value. If not, we default to 6. We also validate the input — a dice with fewer than 2 sides does not make sense, so we send an ephemeral error message (one that only the user who ran the command can see).
Notice the difference from HTTP request-response. With a Spring controller, you return an object and Spring sends it. With Discord, you call event.reply() or event.replyEmbeds() and then .queue(). The interaction model is slightly different, but the concept is the same: receive a request, process it, send a response. The fact that your "response" is a colorful embed in a chat channel instead of JSON in a browser does not change the underlying pattern.
Run your bot again. In Discord, type / and you should see your three commands appear in the menu. Try each one. /greet will show a beautiful embed with the user's avatar. /roll will roll a dice (try /roll sides:20 for a d20). /quote will display a random inspirational quote. Your bot is starting to feel real.
5. Building Something Useful
A greeting command and a dice roller are fun, but they are toys. Let us build commands that are genuinely useful — the kind of features that make people actually want to add your bot to their server. We will build three: /help, /remind, and /poll.
The /help Command
Every bot needs a help command. It should list every available command with a short description. This is your bot's documentation, and it should be clean and easy to read.
First, register the command in your main class (add it to the updateCommands() call):
// Add this to your updateCommands() call alongside the others
Commands.slash("help", "Show all available bot commands")
Then add the handler in your SlashCommandListener:
private void handleHelp(SlashCommandInteractionEvent event) {
EmbedBuilder embed = new EmbedBuilder()
.setTitle("Bot Commands")
.setDescription("Here is everything I can do:")
.setColor(Color.decode("#5865F2"))
.addField("/help", "Show this help message", false)
.addField("/greet", "Get a friendly greeting with your info", false)
.addField("/roll [sides]", "Roll a dice (default: 6 sides)", false)
.addField("/quote", "Get a random inspirational quote", false)
.addField("/remind <minutes> <message>",
"Set a reminder. I will ping you when the time is up.", false)
.addField("/poll <question> <option1> <option2> [option3] [option4]",
"Create a poll that members can vote on with reactions", false)
.setFooter("Built with Java and JDA at Coders Farm");
event.replyEmbeds(embed.build()).queue();
}
Simple. Clean. Every embed field has the command syntax and a description. When you add new commands later, you update this help embed. Notice that we list all commands including the ones we have not built yet — /remind and /poll. That is intentional. We are working toward something, and this help command is our roadmap.
The /remind Command
This is where things get interesting. A reminder command needs to wait for a specified amount of time and then send a message. This is a perfect use case for ScheduledExecutorService, a Java concurrency tool that lets you schedule tasks to run after a delay.
First, register the command:
// Add to updateCommands()
Commands.slash("remind", "Set a reminder")
.addOption(OptionType.INTEGER, "minutes",
"How many minutes from now", true)
.addOption(OptionType.STRING, "message",
"What to remind you about", true)
Note the true at the end of each addOption call. That makes these options required. Discord will not let a user run /remind without providing both values. The framework does your input validation for you.
Now the handler. Add a ScheduledExecutorService field to your SlashCommandListener class and the handler method:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class SlashCommandListener extends ListenerAdapter {
private final Random random = new Random();
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2);
// ... existing code ...
private void handleRemind(SlashCommandInteractionEvent event) {
int minutes = event.getOption("minutes").getAsInt();
String message = event.getOption("message").getAsString();
String userId = event.getUser().getId();
long channelId = event.getChannel().getIdLong();
if (minutes < 1 || minutes > 1440) {
event.reply("Please set a reminder between 1 and 1440 minutes (24 hours).")
.setEphemeral(true).queue();
return;
}
// Acknowledge the command immediately
event.reply("Got it! I will remind you in **" + minutes
+ " minute" + (minutes == 1 ? "" : "s")
+ "** about: " + message).queue();
// Schedule the reminder
scheduler.schedule(() -> {
event.getJDA().getTextChannelById(channelId)
.sendMessage("<@" + userId + "> **Reminder:** " + message)
.queue();
}, minutes, TimeUnit.MINUTES);
}
}
Let us study what is happening here:
- We validate that the time is between 1 minute and 24 hours. Always validate user input, even when Discord handles type checking for you.
- We reply immediately to acknowledge the command. Discord requires a response within 3 seconds or the interaction fails. You cannot wait 30 minutes to respond.
- We use
scheduler.schedule()to run a lambda function after the specified delay. The lambda captures the user ID and channel ID so it can send a message later. - The
<@userId>syntax creates a mention — it will ping the user with a notification. That is the whole point of a reminder.
ScheduledExecutorService is a tool from java.util.concurrent that manages a pool of threads for scheduled tasks. newScheduledThreadPool(2) creates a pool with 2 threads, meaning your bot can handle multiple pending reminders simultaneously. This is the same concurrency model that powers things like cache expiration, periodic health checks, and session timeouts in professional Java applications.
The /poll Command
A poll command lets users create a question with options, and other users vote by clicking emoji reactions. This is one of the most popular features in real Discord bots, and it ties together several concepts: slash command options, embeds, and reactions.
Register the command:
// Add to updateCommands()
Commands.slash("poll", "Create a poll")
.addOption(OptionType.STRING, "question", "The poll question", true)
.addOption(OptionType.STRING, "option1", "First option", true)
.addOption(OptionType.STRING, "option2", "Second option", true)
.addOption(OptionType.STRING, "option3", "Third option (optional)", false)
.addOption(OptionType.STRING, "option4", "Fourth option (optional)", false)
And the handler:
private void handlePoll(SlashCommandInteractionEvent event) {
String question = event.getOption("question").getAsString();
// Collect all provided options
String[] emojis = {"1\u20E3", "2\u20E3", "3\u20E3", "4\u20E3"};
List<String> options = new java.util.ArrayList<>();
for (int i = 1; i <= 4; i++) {
var opt = event.getOption("option" + i);
if (opt != null) {
options.add(opt.getAsString());
}
}
// Build the poll description
StringBuilder description = new StringBuilder();
for (int i = 0; i < options.size(); i++) {
description.append(emojis[i])
.append(" ")
.append(options.get(i))
.append("\n\n");
}
EmbedBuilder embed = new EmbedBuilder()
.setTitle("Poll: " + question)
.setDescription(description.toString())
.setColor(Color.decode("#E67E22"))
.setFooter("Poll by " + event.getUser().getName()
+ " — React to vote!");
// Send the embed, then add reaction emojis
event.replyEmbeds(embed.build()).queue(hook -> {
hook.retrieveOriginal().queue(msg -> {
for (int i = 0; i < options.size(); i++) {
msg.addReaction(net.dv8tion.jda.api.entities.emoji
.Emoji.fromUnicode(emojis[i])).queue();
}
});
});
}
This is the most complex command so far, so let us walk through it carefully:
- We gather 2 to 4 options from the command arguments. The first two are required; the last two are optional.
- We build a description string with numbered emoji next to each option. The
\u20E3is the Unicode "combining enclosing keycap" character — when combined with a digit, it creates the keycap emoji (1, 2, 3, 4). - We send the embed as a reply, and then in the callback, we retrieve the actual message object and add reaction emojis to it. Users vote by clicking these reactions.
- The callback chain (
.queue(hook -> hook.retrieveOriginal().queue(msg -> ...))) is necessary because we need the message object to add reactions, and we do not have it until after the reply is sent. This is asynchronous programming in action.
Do not forget to add cases for your new commands in the switch block of onSlashCommandInteraction:
@Override
public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
switch (event.getName()) {
case "greet" -> handleGreet(event);
case "roll" -> handleRoll(event);
case "quote" -> handleQuote(event);
case "help" -> handleHelp(event);
case "remind" -> handleRemind(event);
case "poll" -> handlePoll(event);
}
}
Run the bot. Try /poll question:What should we code next? option1:Discord bot option2:Android app option3:CLI tool. You will see a beautiful orange embed with the question and three numbered options, each with a reaction emoji already attached. Click one to vote. That is your poll system, and you built it.
6. Connecting to Existing Skills
Here is where this side quest comes full circle. Everything you learned in the Resumator lessons — databases, APIs, REST calls — works just as well inside a Discord bot. A bot is just another interface to your backend skills. Let us prove it.
Database Integration: Saving Poll Results
You already know how to work with MySQL. You built entities, repositories, and service classes. Let us save poll results to a database so they persist even if the bot restarts.
If you are using Spring Boot for your bot project (recommended), add the Spring Data JPA and MySQL dependencies to your pom.xml just like you did for Resumator. Then create an entity:
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "polls")
public class Poll {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "question", nullable = false)
private String question;
@Column(name = "options", nullable = false, length = 1000)
private String options; // Stored as comma-separated values
@Column(name = "message_id", nullable = false)
private String messageId; // Discord message ID for tracking votes
@Column(name = "channel_id", nullable = false)
private String channelId;
@Column(name = "created_by", nullable = false)
private String createdBy;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
}
// Default constructor required by JPA
public Poll() {}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getQuestion() { return question; }
public void setQuestion(String question) { this.question = question; }
public String getOptions() { return options; }
public void setOptions(String options) { this.options = options; }
public String getMessageId() { return messageId; }
public void setMessageId(String messageId) { this.messageId = messageId; }
public String getChannelId() { return channelId; }
public void setChannelId(String channelId) { this.channelId = channelId; }
public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
And a repository:
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface PollRepository extends JpaRepository<Poll, Long> {
Optional<Poll> findByMessageId(String messageId);
}
This is exactly the same pattern from Resumator. Entity with @Entity and @Table, repository extending JpaRepository, custom finder method that Spring generates from the method name. The database does not care whether the data comes from a web form or a Discord command. Data is data.
Now update your poll handler to save to the database after creating a poll. Inject the PollRepository into your listener and save the poll data in the callback after the message is sent. You know how to do this — it is the same repository.save() call you have written dozens of times.
API Integration: A /weather Command
In Resumator, you used RestTemplate to call the JSearch API and get job listings. The exact same approach works for any external API. Let us add a /weather command that calls a weather API.
Register the command:
// Add to updateCommands()
Commands.slash("weather", "Get the current weather for a city")
.addOption(OptionType.STRING, "city", "City name", true)
And the handler (using a free weather API like wttr.in or OpenWeatherMap):
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
private void handleWeather(SlashCommandInteractionEvent event) {
String city = event.getOption("city").getAsString();
// Defer the reply because the API call might take a moment
event.deferReply().queue();
try {
RestTemplate restTemplate = new RestTemplate();
String url = "https://wttr.in/" + city + "?format=j1";
String response = restTemplate.getForObject(url, String.class);
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(response);
JsonNode current = root.get("current_condition").get(0);
String temp = current.get("temp_F").asText();
String feelsLike = current.get("FeelsLikeF").asText();
String description = current.get("weatherDesc").get(0)
.get("value").asText();
String humidity = current.get("humidity").asText();
String wind = current.get("windspeedMiles").asText();
EmbedBuilder embed = new EmbedBuilder()
.setTitle("Weather in " + city)
.setDescription("**" + description + "**")
.addField("Temperature", temp + "\u00B0F", true)
.addField("Feels Like", feelsLike + "\u00B0F", true)
.addField("Humidity", humidity + "%", true)
.addField("Wind", wind + " mph", true)
.setColor(Color.decode("#3498DB"))
.setFooter("Requested by " + event.getUser().getName());
event.getHook().sendMessageEmbeds(embed.build()).queue();
} catch (Exception e) {
event.getHook().sendMessage(
"Could not get weather for **" + city
+ "**. Check the city name and try again.").queue();
}
}
There is one new concept here: deferReply(). Discord requires a response within 3 seconds. API calls to external services might take longer than that. deferReply() tells Discord "I received the command, I am working on it" — the user sees a "Bot is thinking..." message. You then have up to 15 minutes to send the actual response using event.getHook().sendMessage() instead of event.reply().
Look at the RestTemplate call. It is the exact same class you used in Resumator to call the JSearch API. The ObjectMapper to parse JSON? Same as Resumator. The try-catch to handle errors gracefully? Same pattern. You are using your existing skills in a completely new context. That is the whole point.
A Bot Is Just Another Interface
This is worth saying explicitly: a Discord bot is not some exotic, specialized thing. It is a Java application that receives input and produces output. The input comes from Discord events instead of HTTP requests. The output goes to Discord channels instead of web browsers. But everything in between — the business logic, the database calls, the API integrations, the data processing — is identical to what you already know. When you learn to build a REST API, you are also learning to build a Discord bot, a CLI tool, a Slack app, or anything else. The interface changes. The skills do not.
7. Deployment Considerations
There is one fundamental difference between a Discord bot and a web application you access through a browser: a bot needs to run all the time.
When you built Resumator, you ran it on your laptop. You started the Spring Boot application, used it, and when you closed IntelliJ, the server stopped. That was fine because you were the only user and you only needed it while you were actively developing. A Discord bot is different. If your bot is in a server with other people, they expect it to respond at 3am on a Saturday. If your Java process is not running, the bot appears offline and commands fail.
So where does the bot actually run? You have several options:
Option 1: Your Own Machine
The simplest approach during development. You run the bot on your computer. The downside is obvious: when you close your laptop or restart, the bot goes down. This is fine for testing, but not for a bot other people rely on.
Option 2: A Virtual Private Server (VPS)
Services like DigitalOcean, Linode, or Vultr give you a small Linux server for $4-6 per month. You SSH into it, upload your compiled JAR file, and run it. The server runs 24/7, so your bot does too. This is the most common choice for hobby bots.
Option 3: Cloud Free Tiers
Oracle Cloud offers an "always free" tier that includes a small Linux VM. AWS, Google Cloud, and Azure also have free tiers with some limitations. These can run a small Java bot comfortably at no cost, though setup can be more complex.
Keeping It Running
Once your bot is on a server, you need to make sure it stays running even if you disconnect your SSH session, and restarts automatically if it crashes. There are several approaches:
screenortmux: Terminal multiplexers that let you start a process, detach from it, and reconnect later. Simple but no auto-restart on crash.systemd: The Linux service manager. You create a service file that tells Linux to run your JAR, restart it if it crashes, and start it on boot. This is the professional approach for Linux servers.- Docker: Package your bot as a container with
--restart=always. Docker handles restarts and makes deployment reproducible. If you learn Docker (another great side quest), this is the cleanest option.
For now, running the bot on your local machine is perfectly fine. The important thing is that you understand the why behind deployment: a bot is a long-running process, not a request-response cycle. That shapes how you think about error handling (catch exceptions so the whole process does not die), resource management (close database connections, limit thread pools), and logging (you need to know what happened at 3am when you were asleep).
Knowledge Check
1. Why do we check event.getAuthor().isBot() at the beginning of the onMessageReceived method and return early if true?
isBot() and returning early prevents this. It is a fundamental safety check that every Discord bot should have.2. What is the purpose of calling event.deferReply() before making an external API call in a slash command handler?
deferReply() sends an immediate acknowledgment ("Bot is thinking...") and gives you up to 15 minutes to send the actual response via event.getHook(). This is a common pattern whenever your command involves network calls, database queries, or any operation that might be slow.3. How does building a Discord bot connect to the REST API skills you learned in Resumator?
RestTemplate, JPA repositories, and ObjectMapper work identically in both contexts. Learning to build one type of application gives you the skills to build many others — only the interface layer changes.Deliverable
A working Discord bot running in a real Discord server with at least 4 slash commands. Your bot should use JDA, respond with formatted embeds, and demonstrate at least one of: database integration, external API calls, or scheduled tasks. Invite your Coders Farm friends to your test server and let them try your commands. There is nothing like watching other people use something you built.
Finished this side quest?