git » subscription-tool.git » commit d2cc48d

updated and added documentation

author Thorsten Ortlepp
2026-02-12 20:38:20 UTC
committer Thorsten Ortlepp
2026-02-12 20:38:20 UTC
parent d2bd9524fb0347e5374c8432262c50c20af3fafc

updated and added documentation

LICENSE.md +1 -1
README.md +54 -1
src/main/java/dev/rubidium/subscriptiontool/configuration/MessageSourceConfiguration.java +8 -0
src/main/java/dev/rubidium/subscriptiontool/configuration/SecurityConfiguration.java +14 -0
src/main/java/dev/rubidium/subscriptiontool/controller/CustomErrorController.java +9 -2
src/main/java/dev/rubidium/subscriptiontool/controller/ManagementController.java +17 -1
src/main/java/dev/rubidium/subscriptiontool/controller/SubscriptionController.java +46 -3
src/main/java/dev/rubidium/subscriptiontool/persistence/repository/MailRepository.java +14 -0
src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java +20 -0
src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java +8 -3
src/main/java/dev/rubidium/subscriptiontool/service/CodeService.java +9 -0
src/main/java/dev/rubidium/subscriptiontool/service/MailService.java +12 -0
src/main/java/dev/rubidium/subscriptiontool/service/ManagementService.java +18 -0
src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java +54 -2
src/main/java/dev/rubidium/subscriptiontool/service/SubscriptionService.java +22 -1
src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java +3 -3
src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java +3 -3

diff --git a/LICENSE.md b/LICENSE.md
index b87f10e..b410c83 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
 # MIT License
 
-Copyright 2025 Thorsten Ortlepp
+Copyright 2025-2026 Thorsten Ortlepp
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software
  and associated documentation files (the “Software”), to deal in the Software without restriction,
diff --git a/README.md b/README.md
index 21144e7..a08da3d 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,26 @@
 # Subscription-Tool
 
-A Spring Boot tool to manage subscriptions (e.g. to a newsletter).
+A rather simple Spring Boot application to manage subscriptions (e.g. to a newsletter).
+
+
+## Usage
+
+On `/subscribe` a new subscription can be added. Before a subscription becomes active,
+ the double opt-in process has to be completed by clicking on the link in the confirmation email.
+
+On `/unsubscribe` an existing subscription can be removed.
+
+On `/manage` all subscriptions can be managed. It is possible to activate subscriptions
+ (skipping the double opt-in process) and delete subscriptions. A list of all active
+ subscriptions can be copied to the clipboard to be used in an email.
+
 
 ## Setup
 
+### Requirements
+
+To run the application, Java 25 or higher, a PostgreSQL database and an SMTP server are required.
+
 ### Database
 
 A PostgreSQL database is required to run the application. To set up a local database, you can use
@@ -17,6 +34,8 @@ CREATE DATABASE subscriptiontool;
 
 ### Configuration
 
+Change the configuration values in `application.properties` to match your environment.
+
 Basic authentication protects private endpoints (e.g., `/actuator`). The username and password can
  be configured by using the following properties:
 
@@ -25,3 +44,37 @@ Basic authentication protects private endpoints (e.g., `/actuator`). The usernam
 
 The password is hashed using Bcrypt. To create a password hash, use the following command:
  `mkpasswd -m bcrypt` (on Debian GNU/Linux `mkpasswd` is part of the *whois* package).
+
+### Translation
+
+Translation to other languages can be done by editing the `messages.properties` file.
+
+### systemd service
+
+To run the application as a systemd service, create a dedicated user (e.g. *spring*) first.
+ Then place the executable JAR file in `/opt/spring` and create a service unit:
+
+```
+[Unit]
+Description=subscription-tool
+After=syslog.target
+
+[Service]
+User=spring
+ExecStart=java -Dserver.port=8090 -jar /opt/spring/subscription-tool.jar
+SuccessExitStatus=143 
+
+[Install] 
+WantedBy=multi-user.target
+```
+(`/etc/systemd/system/subscription-tool.service`)
+
+Use `systemctl enable subscription-tool.service` to enable the service. When running, the
+ application will be available at [http://localhost:8090/](http://localhost:8090/).
+
+
+## Security
+
+When the application is exposed to the internet, it should be used behind a reverse proxy.
+ For example, use nginx to handle secure TLS connections to the application. Also, set up
+ a reasonable rate limit to avoid DOS and brute force attacks.
diff --git a/src/main/java/dev/rubidium/subscriptiontool/configuration/MessageSourceConfiguration.java b/src/main/java/dev/rubidium/subscriptiontool/configuration/MessageSourceConfiguration.java
index d48bce6..57a2aca 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/configuration/MessageSourceConfiguration.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/configuration/MessageSourceConfiguration.java
@@ -4,9 +4,17 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.support.ResourceBundleMessageSource;
 
+/**
+ * Configure internationalization.
+ */
 @Configuration
 public class MessageSourceConfiguration {
 
+  /**
+   * Configure messages.properties as messages source.
+   *
+   * @return The configured message source
+   */
   @Bean
   public ResourceBundleMessageSource messageSource() {
     ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
diff --git a/src/main/java/dev/rubidium/subscriptiontool/configuration/SecurityConfiguration.java b/src/main/java/dev/rubidium/subscriptiontool/configuration/SecurityConfiguration.java
index ebbf3ec..fa41f3d 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/configuration/SecurityConfiguration.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/configuration/SecurityConfiguration.java
@@ -12,6 +12,9 @@ import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
 import org.springframework.security.web.SecurityFilterChain;
 
+/**
+ * Configure security.
+ */
 @Configuration
 @EnableWebSecurity
 public class SecurityConfiguration {
@@ -24,6 +27,12 @@ public class SecurityConfiguration {
   @Value("${subscriptiontool.web.password}")
   private String password;
 
+  /**
+   * Protect the actuator and management endpoints with HTTP Basic authentication.
+   *
+   * @param httpSecurity HttpSecurity object
+   * @return Configured HttpSecurity object
+   */
   @Bean
   public SecurityFilterChain defaultHttpSecurity(HttpSecurity httpSecurity) {
     httpSecurity
@@ -36,6 +45,11 @@ public class SecurityConfiguration {
     return httpSecurity.build();
   }
 
+  /**
+   * Build user manager for protected endpoints.
+   *
+   * @return User manager for authentication
+   */
   @Bean
   public UserDetailsService users() {
     UserDetails user = User.builder()
diff --git a/src/main/java/dev/rubidium/subscriptiontool/controller/CustomErrorController.java b/src/main/java/dev/rubidium/subscriptiontool/controller/CustomErrorController.java
index 2ae005c..b60c2ef 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/controller/CustomErrorController.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/controller/CustomErrorController.java
@@ -2,14 +2,21 @@ package dev.rubidium.subscriptiontool.controller;
 
 import org.springframework.boot.webmvc.error.ErrorController;
 import org.springframework.stereotype.Controller;
-import org.springframework.ui.Model;
 import org.springframework.web.bind.annotation.RequestMapping;
 
+/**
+ * Controller to handle errors.
+ */
 @Controller
 public class CustomErrorController implements ErrorController {
 
+  /**
+   * Handle errors and display the error page.
+   *
+   * @return The name of the related view: 'Error'
+   */
   @RequestMapping("/error")
-  public String handleError(Model model) {
+  public String handleError() {
     return "Error";
   }
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/controller/ManagementController.java b/src/main/java/dev/rubidium/subscriptiontool/controller/ManagementController.java
index fb1600b..b66b089 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/controller/ManagementController.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/controller/ManagementController.java
@@ -12,10 +12,12 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.ModelAttribute;
 import org.springframework.web.bind.annotation.PostMapping;
 
+/**
+ * Controller to manage subscriptions by administrators.
+ */
 @Controller
 public class ManagementController {
 
-  private static final String ATTRIBUTE_NAME_TRANSLATION = "translation";
   private static final String ATTRIBUTE_NAME_SUBSCRIBERS = "subscribers";
   private static final String ATTRIBUTE_NAME_MAILS = "mails";
 
@@ -25,6 +27,12 @@ public class ManagementController {
     this.managementService = managementService;
   }
 
+  /**
+   * Endpoint to display the management page.
+   *
+   * @param model Data for the view
+   * @return The name of the related view: 'Manage'
+   */
   @GetMapping("/manage")
   public String index(Model model) {
     var subscribers = managementService.getSubscribers();
@@ -35,6 +43,14 @@ public class ManagementController {
     return "Manage";
   }
 
+  /**
+   * Endpoint to perform management actions. Depending on the action, the subscription will be
+   * activated or deleted. When the action is completed, the management page is displayed.
+   *
+   * @param managementAction The action to perform, including the affected subscription
+   * @param model            Data for the view
+   * @return The name of the related view: 'Manage'
+   */
   @PostMapping("/manage")
   public String action(@ModelAttribute ManagementAction managementAction, Model model) {
     if (managementAction.getAction() == Action.ACTIVATE) {
diff --git a/src/main/java/dev/rubidium/subscriptiontool/controller/SubscriptionController.java b/src/main/java/dev/rubidium/subscriptiontool/controller/SubscriptionController.java
index f59b554..597ebb6 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/controller/SubscriptionController.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/controller/SubscriptionController.java
@@ -9,10 +9,12 @@ import org.springframework.web.bind.annotation.ModelAttribute;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 
+/**
+ * Controller to manage subscriptions by users.
+ */
 @Controller
 public class SubscriptionController {
 
-  private static final String ATTRIBUTE_NAME_TRANSLATION = "translation";
   private static final String ATTRIBUTE_NAME_SUBSCRIPTION = "subscription";
   private static final String ATTRIBUTE_NAME_SUCCESS = "success";
   private static final String ATTRIBUTE_NAME_EMAIL = "email";
@@ -23,35 +25,68 @@ public class SubscriptionController {
     this.subscriptionService = subscriptionService;
   }
 
+  /**
+   * Endpoint to display the main / index page.
+   *
+   * @return The name of the related view: 'Index'
+   */
   @GetMapping("/")
-  public String index(Model model) {
+  public String index() {
     return "Index";
   }
 
+  /**
+   * Endpoint to display the 'subscribe' page.
+   *
+   * @param model Data for the view
+   * @return The name of the related view: 'SubscribeForm'
+   */
   @GetMapping("/subscribe")
   public String subscribeForm(Model model) {
     model.addAttribute(ATTRIBUTE_NAME_SUBSCRIPTION, new Subscription());
     return "SubscribeForm";
   }
 
+  /**
+   * Endpoint to create a new subscription. When the creation is completed, the 'subscription saved'
+   * page is displayed.
+   *
+   * @param subscription Data of the subscription to create
+   * @param model        Data for the view
+   * @return The name of the related view: 'SubscribeSave'
+   */
   @PostMapping("/subscribe")
   public String subscribeSave(@ModelAttribute Subscription subscription, Model model) {
     boolean saved = false;
     String email = cleanInput(subscription.getEmail());
     if (!email.isEmpty()) {
-      saved = subscriptionService.saveSubscription(email);
+      saved = subscriptionService.createSubscription(email);
     }
     model.addAttribute(ATTRIBUTE_NAME_EMAIL, email);
     model.addAttribute(ATTRIBUTE_NAME_SUCCESS, saved);
     return "SubscribeSave";
   }
 
+  /**
+   * Endpoint to display the 'unsubscribe' page.
+   *
+   * @param model Data for the view
+   * @return The name of the related view: 'UnsubscribeForm'
+   */
   @GetMapping("/unsubscribe")
   public String unsubscribeForm(Model model) {
     model.addAttribute(ATTRIBUTE_NAME_SUBSCRIPTION, new Subscription());
     return "UnsubscribeForm";
   }
 
+  /**
+   * Endpoint to delete an existing subscription. When the deletion is completed, the 'subscription
+   * deleted' page is displayed.
+   *
+   * @param subscription Data of the subscription to delete
+   * @param model        Data for the view
+   * @return The name of the related view: 'UnsubscribeDelete'
+   */
   @PostMapping("/unsubscribe")
   public String unsubscribeDelete(@ModelAttribute Subscription subscription, Model model) {
     boolean deleted = false;
@@ -64,6 +99,14 @@ public class SubscriptionController {
     return "UnsubscribeDelete";
   }
 
+  /**
+   * Endpoint to confirm / activate a subscription. When the confirmation is completed, the
+   * 'confirmation' page is displayed.
+   *
+   * @param code  Confirmation code of the subscription to confirm
+   * @param model Data for the view
+   * @return The name of the related view: 'Confirm'
+   */
   @GetMapping("/confirm")
   public String confirm(@RequestParam(name = "code", required = false) String code, Model model) {
     boolean confirmed = false;
diff --git a/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/MailRepository.java b/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/MailRepository.java
index 3e77673..6131277 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/MailRepository.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/MailRepository.java
@@ -5,11 +5,25 @@ import java.util.List;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
 
+/**
+ * Repository for double opt-in emails.
+ */
 public interface MailRepository extends JpaRepository<Mail, Long> {
 
+  /**
+   * Find an email by subscription id.
+   *
+   * @param id Subscription id
+   * @return Found email or null if not found
+   */
   @Query("SELECT m FROM Mail m WHERE m.subscription = :id")
   Mail findBySubscription(Long id);
 
+  /**
+   * Find all unsent emails.
+   *
+   * @return List of all unsent emails
+   */
   @Query("SELECT m FROM Mail m WHERE m.sent = false")
   List<Mail> findUnsentMails();
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java b/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java
index 60e281c..598c30a 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/persistence/repository/SubscriptionRepository.java
@@ -5,14 +5,34 @@ import java.util.List;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
 
+/**
+ * Repository for subscriptions.
+ */
 public interface SubscriptionRepository extends JpaRepository<Subscription, Long> {
 
+  /**
+   * Find a subscription by its email address.
+   *
+   * @param mail Email address
+   * @return Found subscription or null if not found
+   */
   @Query("SELECT s FROM Subscription s WHERE s.mail = :mail")
   Subscription findByMail(String mail);
 
+  /**
+   * Find a subscription by its confirmation code.
+   *
+   * @param code Monfirmation code
+   * @return Found subscription or null if not found
+   */
   @Query("SELECT s FROM Subscription s WHERE s.code = :code")
   Subscription findByCode(String code);
 
+  /**
+   * Find all subscriptions, including their double opt-in emails (if existing), ordered by ID.
+   *
+   * @return List of all subscriptions
+   */
   @Query("SELECT s FROM Subscription s LEFT JOIN s.relationMail ORDER BY s.id ASC")
   List<Subscription> findAllSubscriptions();
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java b/src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java
index 5715e7d..58ae072 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/scheduler/MailScheduler.java
@@ -11,6 +11,9 @@ import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
+/**
+ * Scheduler for sending double opt-in emails.
+ */
 @Service
 @EnableScheduling
 public class MailScheduler {
@@ -34,6 +37,9 @@ public class MailScheduler {
     this.messageSource = messageSource;
   }
 
+  /**
+   * Send all unsent double opt-in emails. Triggered by cron expression.
+   */
   @Scheduled(cron = "${subscriptiontool.scheduler.cron}",
       zone = "${subscriptiontool.scheduler.zone}")
   public void sendMails() {
@@ -43,9 +49,8 @@ public class MailScheduler {
       String message =
           messageSource.getMessage("mail.body", null, Locale.getDefault()).formatted(link);
       String subject = messageSource.getMessage("mail.subject", null, Locale.getDefault());
-      if (mailService.sendMail(
-          mailFrom, mail.email(), subject, message)) {
-        persistenceService.updateMail(mail.id());
+      if (mailService.sendMail(mailFrom, mail.email(), subject, message)) {
+        persistenceService.updateMailStatus(mail.id());
       }
     });
   }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/CodeService.java b/src/main/java/dev/rubidium/subscriptiontool/service/CodeService.java
index 3224e3a..b0f6303 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/service/CodeService.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/service/CodeService.java
@@ -1,6 +1,15 @@
 package dev.rubidium.subscriptiontool.service;
 
+/**
+ * Service to generate codes to be used as identifiers.
+ */
 public interface CodeService {
 
+  /**
+   * Generate a unique code from an input string.
+   *
+   * @param input Input string for the code generation
+   * @return Generated unique code
+   */
   String generateCode(String input);
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/MailService.java b/src/main/java/dev/rubidium/subscriptiontool/service/MailService.java
index 1d31e4c..690af63 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/service/MailService.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/service/MailService.java
@@ -1,6 +1,18 @@
 package dev.rubidium.subscriptiontool.service;
 
+/**
+ * Service to create and send emails.
+ */
 public interface MailService {
 
+  /**
+   * Create and send an email.
+   *
+   * @param from    Sender email address
+   * @param to      Recipient email address
+   * @param subject Subject of the email
+   * @param body    Body of the email
+   * @return Email sent status; true = success, false = failure
+   */
   boolean sendMail(String from, String to, String subject, String body);
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/ManagementService.java b/src/main/java/dev/rubidium/subscriptiontool/service/ManagementService.java
index f9deefe..239b821 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/service/ManagementService.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/service/ManagementService.java
@@ -3,11 +3,29 @@ package dev.rubidium.subscriptiontool.service;
 import dev.rubidium.subscriptiontool.model.Subscriber;
 import java.util.List;
 
+/**
+ * Service to manage subscriptions by administrators.
+ */
 public interface ManagementService {
 
+  /**
+   * Retrieve a list of all subscribers.
+   *
+   * @return List of all subscribers
+   */
   List<Subscriber> getSubscribers();
 
+  /**
+   * Activate a subscription (confirm double opt-in).
+   *
+   * @param id ID of the subscription to activate
+   */
   void activateSubscription(Long id);
 
+  /**
+   * Delete a subscription (unsubscribe).
+   *
+   * @param id ID of the subscription to delete
+   */
   void deleteSubscription(Long id);
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java b/src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java
index f84896e..08d22c1 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/service/PersistenceService.java
@@ -4,21 +4,73 @@ import dev.rubidium.subscriptiontool.model.Subscriber;
 import dev.rubidium.subscriptiontool.model.UnsentMail;
 import java.util.List;
 
+/**
+ * Service to manage the persistence of subscriptions.
+ */
 public interface PersistenceService {
 
-  boolean createSubscription(String email, String code);
+  /**
+   * Save a subscription. If a subscription with the same email address already exists, no new
+   * subscription is created.
+   *
+   * @param email The email address to identify the subscription
+   * @param code  The confirmation code to identify the subscription
+   * @return Save status; true = successfully saved, false = failure
+   */
+  boolean saveSubscription(String email, String code);
 
+  /**
+   * Delete a subscription identified by its ID.
+   *
+   * @param id The ID of the subscription to delete
+   * @return Deletion status; true = successfully deleted, false = failure
+   */
   boolean deleteSubscription(Long id);
 
+  /**
+   * Delete a subscription identified by its email address.
+   *
+   * @param email The email address of the subscription to delete
+   * @return Deletion status; true = successfully deleted, false = failure
+   */
   boolean deleteSubscription(String email);
 
+  /**
+   * Change the confirmation status of a subscription identified by its ID to confirmed (confirm
+   * double opt-in).
+   *
+   * @param id The ID of the subscription to confirm
+   * @return Confirmation status; true = successfully confirmed, false = failure
+   */
   boolean confirmSubscription(Long id);
 
+  /**
+   * Change the confirmation status of a subscription identified by its confirmation code to
+   * confirmed (confirm double opt-in).
+   *
+   * @param code The confirmation code of the subscription to confirm
+   * @return Confirmation status; true = successfully confirmed, false = failure
+   */
   boolean confirmSubscription(String code);
 
+  /**
+   * Get a list of all unsent confirmation emails.
+   *
+   * @return List of all unsent confirmation emails
+   */
   List<UnsentMail> getUnsentMails();
 
-  void updateMail(Long id);
+  /**
+   * Update the status of a confirmation email to successfully send.
+   *
+   * @param id The ID of the confirmation email to update
+   */
+  void updateMailStatus(Long id);
 
+  /**
+   * Retrieve a list of all subscribers.
+   *
+   * @return List of all subscribers
+   */
   List<Subscriber> getSubscribers();
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/SubscriptionService.java b/src/main/java/dev/rubidium/subscriptiontool/service/SubscriptionService.java
index 9662616..317d52d 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/service/SubscriptionService.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/service/SubscriptionService.java
@@ -1,10 +1,31 @@
 package dev.rubidium.subscriptiontool.service;
 
+/**
+ * Service to manage subscriptions by users.
+ */
 public interface SubscriptionService {
 
-  boolean saveSubscription(String email);
+  /**
+   * Create a new subscription (subscribe).
+   *
+   * @param email The email address to identify the subscription
+   * @return Creation status; true = successfully created, false = failure
+   */
+  boolean createSubscription(String email);
 
+  /**
+   * Delete an existing subscription (unsubscribe).
+   *
+   * @param email The email address to identify the subscription
+   * @return Deletion status; true = successfully deleted, false = failure
+   */
   boolean deleteSubscription(String email);
 
+  /**
+   * Confirm (activate) a subscription.
+   *
+   * @param code The confirmation code to identify the subscription
+   * @return Confirmation status; true = successfully confirmed, false = failure
+   */
   boolean confirmSubscription(String code);
 }
diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java b/src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java
index cf5ae20..563fef5 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/service/impl/PersistenceServiceImpl.java
@@ -24,14 +24,14 @@ public class PersistenceServiceImpl implements PersistenceService {
   private final SubscriptionRepository subscriptionRepository;
   private final MailRepository mailRepository;
 
-  PersistenceServiceImpl(final SubscriptionRepository subscriptionRepository,
+  public PersistenceServiceImpl(final SubscriptionRepository subscriptionRepository,
       final MailRepository mailRepository) {
     this.subscriptionRepository = subscriptionRepository;
     this.mailRepository = mailRepository;
   }
 
   @Override
-  public boolean createSubscription(String email, String code) {
+  public boolean saveSubscription(String email, String code) {
     Subscription registered = subscriptionRepository.findByMail(email);
     if (registered == null) {
       Subscription subscription = new Subscription();
@@ -124,7 +124,7 @@ public class PersistenceServiceImpl implements PersistenceService {
   }
 
   @Override
-  public void updateMail(Long id) {
+  public void updateMailStatus(Long id) {
     mailRepository.findById(id).ifPresent(mail -> {
       mail.setSent(Boolean.TRUE);
       mail.setCompletion(Timestamp.valueOf(LocalDateTime.now()));
diff --git a/src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java b/src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java
index 9fe52bc..8886979 100644
--- a/src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java
+++ b/src/main/java/dev/rubidium/subscriptiontool/service/impl/SubscriptionServiceImpl.java
@@ -23,10 +23,10 @@ public class SubscriptionServiceImpl implements SubscriptionService {
   }
 
   @Override
-  public boolean saveSubscription(String email) {
-    logger.info("Saving a new subscription at {}", LocalDateTime.now());
+  public boolean createSubscription(String email) {
+    logger.info("Creating a new subscription at {}", LocalDateTime.now());
     String code = codeService.generateCode(email);
-    return persistenceService.createSubscription(email, code);
+    return persistenceService.saveSubscription(email, code);
   }
 
   @Override