diff --git a/README.md b/README.md
index ec8ca078f97dbe16a353f49524e78c9fa840f302..82983d7d9934f2f2429252b0110872375a2c40c5 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,12 @@ The first super admin user must be added manually, then he/she will be able to a
 
 The value `user_id` is the RAP user id.
 
+## Hooks
+
+It is possible to load external jar files in order to extend the GMS with custom functionalities (implementing the *Hook interfaces and annotating the class with `@org.springframework.stereotype.Component`). Currently only the `GroupsHook` is available.
+
+External jar files need to be specified at application startup using the `LOADER_PATH` environment variable (this is a Spring feature).
+
 ## Developer notes
 
 Backend and frontend are 2 separate applications:
diff --git a/gms/pom.xml b/gms/pom.xml
index d3842dae4a1961cfcd345115d5519cd4de8bc45a..47d61e3a46cfc29fdfb27805792ebb1e8fbfdfca 100644
--- a/gms/pom.xml
+++ b/gms/pom.xml
@@ -114,6 +114,26 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>3.1.2</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                        <phase>package</phase>
+                        <configuration>
+                            <!-- creates a gms-lib jar that can be included as dependency in other projects -->
+                            <classifier>lib</classifier>
+                            <includes>
+                                <include>**/**</include>
+                            </includes>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
             <plugin>
                 <groupId>com.spotify</groupId>
                 <artifactId>dockerfile-maven-plugin</artifactId>
@@ -130,6 +150,8 @@
                 <artifactId>spring-boot-maven-plugin</artifactId>
                 <configuration>
                     <executable>true</executable>
+                    <!-- layout ZIP is necessary for loading external jar using PropertiesLauncher (used for hooks) -->
+                    <layout>ZIP</layout>
                 </configuration>
             </plugin>
             <plugin>
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java
index fc11b090313df3acddc37608b0ed7ceb58601158..64f838d69b041db31b4fff84d054e0b762415255 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java
@@ -193,7 +193,7 @@ public class JWTWebServiceController {
     @DeleteMapping(value = "/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
     public void deleteGroup(@PathVariable("group") String groupParam, HttpServletResponse response) {
         GroupEntity group = getGroupFromNames(extractGroupNames(groupParam));
-        groupsDAO.deleteGroupById(group.getId());
+        groupsDAO.deleteGroup(group);
         response.setStatus(HttpServletResponse.SC_NO_CONTENT);
     }
 
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java
index 301ebd8cbb61e7165bd5d24ab721be0c39ba89f9..42285935cb9a1324dd87212b566315ce08b8c509 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java
@@ -2,6 +2,7 @@ package it.inaf.ia2.gms.persistence;
 
 import it.inaf.ia2.gms.model.GroupBreadcrumb;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import it.inaf.ia2.gms.service.hook.GroupsHook;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -21,6 +22,9 @@ import org.springframework.stereotype.Component;
 @Component
 public class GroupsDAO {
 
+    @Autowired(required = false)
+    protected GroupsHook groupsHook;
+
     private final JdbcTemplate jdbcTemplate;
 
     @Autowired
@@ -30,6 +34,10 @@ public class GroupsDAO {
 
     public GroupEntity createGroup(GroupEntity group) {
 
+        if (groupsHook != null) {
+            groupsHook.beforeCreate(group);
+        }
+
         String sql = "INSERT INTO gms_group (id, name, path, is_leaf) VALUES (?, ?, ?, ?)";
 
         jdbcTemplate.update(conn -> {
@@ -46,6 +54,10 @@ public class GroupsDAO {
 
     public GroupEntity updateGroup(GroupEntity group) {
 
+        if (groupsHook != null) {
+            groupsHook.beforeUpdate(group);
+        }
+
         String sql = "UPDATE gms_group SET name = ?, path = ?, is_leaf = ? WHERE id = ?";
 
         jdbcTemplate.update(conn -> {
@@ -60,9 +72,14 @@ public class GroupsDAO {
         return group;
     }
 
-    public void deleteGroupById(String groupId) {
+    public void deleteGroup(GroupEntity group) {
+
+        if (groupsHook != null) {
+            groupsHook.beforeDelete(group);
+        }
+
         String sql = "DELETE FROM gms_group WHERE id = ?";
-        jdbcTemplate.update(sql, groupId);
+        jdbcTemplate.update(sql, group.getId());
     }
 
     public Optional<GroupEntity> findGroupById(String groupId) {
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/GroupNameService.java b/gms/src/main/java/it/inaf/ia2/gms/service/GroupNameService.java
index 0cb329b6d85acd5e9f44f52af65d90ff7e76178e..5668fb21a63083f43acd0718666cb8f7d7a18242 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/GroupNameService.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/GroupNameService.java
@@ -3,6 +3,7 @@ package it.inaf.ia2.gms.service;
 import it.inaf.ia2.gms.persistence.GroupsDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -26,6 +27,10 @@ public class GroupNameService {
         return getGroupsNames(groupsDAO.findGroupsByIds(groupIdentifiers));
     }
 
+    public String getGroupCompleteName(GroupEntity group) {
+        return getGroupsNames(Collections.singletonList(group)).get(0);
+    }
+
     /**
      * Returns the list of the group complete names, given a list of GroupEntity
      * objects.
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java
index d6416c95c33fec735675bfd408feeb18baa2c7c0..6ae23bbeab2576d173fcd0757978f03845526bdf 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java
@@ -108,8 +108,8 @@ public class GroupsService {
         membershipsDAO.deleteAllGroupsMembership(groupsToDeleteIds);
         permissionsDAO.deleteAllGroupsPermissions(groupsToDeleteIds);
 
-        for (String groupId : groupsToDeleteIds) {
-            groupsDAO.deleteGroupById(groupId);
+        for (GroupEntity g : groupsToDelete) {
+            groupsDAO.deleteGroup(g);
         }
 
         loggingDAO.logAction("Group deleted, group_id=" + group.getId());
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/hook/GroupsHook.java b/gms/src/main/java/it/inaf/ia2/gms/service/hook/GroupsHook.java
new file mode 100644
index 0000000000000000000000000000000000000000..9f73f091bbd63d6244cff39912a10178afeea250
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/hook/GroupsHook.java
@@ -0,0 +1,12 @@
+package it.inaf.ia2.gms.service.hook;
+
+import it.inaf.ia2.gms.persistence.model.GroupEntity;
+
+public interface GroupsHook {
+
+    void beforeCreate(GroupEntity group);
+
+    void beforeUpdate(GroupEntity group);
+
+    void beforeDelete(GroupEntity group);
+}
diff --git a/gms/src/test/java/it/inaf/ia2/gms/HooksConfig.java b/gms/src/test/java/it/inaf/ia2/gms/HooksConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..6e9e02f747aa8d59db418e130935765ba5bd12c2
--- /dev/null
+++ b/gms/src/test/java/it/inaf/ia2/gms/HooksConfig.java
@@ -0,0 +1,30 @@
+package it.inaf.ia2.gms;
+
+import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import it.inaf.ia2.gms.service.hook.GroupsHook;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class HooksConfig {
+
+    @Bean
+    public GroupsHook groupsHook() {
+        return new GroupsHookTestImpl();
+    }
+
+    public static class GroupsHookTestImpl implements GroupsHook {
+
+        @Override
+        public void beforeCreate(GroupEntity group) {
+        }
+
+        @Override
+        public void beforeUpdate(GroupEntity group) {
+        }
+
+        @Override
+        public void beforeDelete(GroupEntity group) {
+        }
+    }
+}
diff --git a/gms/src/test/java/it/inaf/ia2/gms/controller/JWTWebServiceControllerTest.java b/gms/src/test/java/it/inaf/ia2/gms/controller/JWTWebServiceControllerTest.java
index 5bf8234f6e66d079a85e7ed6c19ae823261c1f3d..625e72c14dc3f3feb6113d24a72e2a309fe791c4 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/controller/JWTWebServiceControllerTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/controller/JWTWebServiceControllerTest.java
@@ -151,7 +151,7 @@ public class JWTWebServiceControllerTest {
         mockMvc.perform(delete("/ws/jwt/LBT.INAF"))
                 .andExpect(status().isNoContent());
 
-        verify(groupsDAO, times(1)).deleteGroupById(eq(inaf.getId()));
+        verify(groupsDAO, times(1)).deleteGroup(eq(inaf));
     }
 
     @Test
diff --git a/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java b/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java
index 271779ebbda4867291013750c1e22743ef377189..fd56e8a6302bab237d6d0ffb69930f77e8a59dd7 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java
@@ -1,8 +1,10 @@
 package it.inaf.ia2.gms.persistence;
 
 import it.inaf.ia2.gms.DataSourceConfig;
+import it.inaf.ia2.gms.HooksConfig;
 import it.inaf.ia2.gms.model.GroupBreadcrumb;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import it.inaf.ia2.gms.service.hook.GroupsHook;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -14,23 +16,32 @@ import static org.junit.Assert.assertTrue;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import org.mockito.internal.util.collections.Sets;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.mock.mockito.SpyBean;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringRunner;
 
 @RunWith(SpringRunner.class)
-@ContextConfiguration(classes = DataSourceConfig.class)
+@ContextConfiguration(classes = {DataSourceConfig.class, HooksConfig.class})
 public class GroupsDAOTest {
 
     @Autowired
     private DataSource dataSource;
 
+    @Autowired
+    @SpyBean
+    private GroupsHook groupsHook;
+
     private GroupsDAO dao;
 
     @Before
     public void setUp() {
         dao = new GroupsDAO(dataSource);
+        dao.groupsHook = groupsHook;
     }
 
     @Test
@@ -43,6 +54,8 @@ public class GroupsDAOTest {
         root.setPath("");
         dao.createGroup(root);
 
+        verify(groupsHook, times(1)).beforeCreate(eq(root));
+
         GroupEntity lbt = new GroupEntity();
         lbt.setId(getNewGroupId());
         lbt.setName("LBT");
@@ -118,6 +131,8 @@ public class GroupsDAOTest {
         tng = dao.findGroupById(tng.getId()).get();
         assertEquals(newName, tng.getName());
 
+        verify(groupsHook, times(1)).beforeUpdate(eq(tng));
+
         // Breadcrumbs
         List<GroupBreadcrumb> breadcrumbs = dao.getBreadcrumbs(lbt.getId() + "." + lbtInaf.getId());
         assertEquals(3, breadcrumbs.size());
@@ -126,9 +141,11 @@ public class GroupsDAOTest {
         assertEquals(lbtInaf.getName(), breadcrumbs.get(2).getGroupName());
 
         // Delete
-        dao.deleteGroupById(lbtInaf.getId());
+        dao.deleteGroup(lbtInaf);
         groups = dao.getDirectSubGroups(lbt.getId());
         assertTrue(groups.isEmpty());
+
+        verify(groupsHook, times(1)).beforeDelete(eq(lbtInaf));
     }
 
     private String getNewGroupId() {