diff --git a/src/Storm/Collection.php b/src/Storm/Collection.php
index 9268caeaf5cebab5aecded253c985b80ebe04318..23a00c646957e269b2276d0213253ff7f2f3fc6d 100644
--- a/src/Storm/Collection.php
+++ b/src/Storm/Collection.php
@@ -26,18 +26,25 @@ THE SOFTWARE.
 
 class Storm_Collection extends ArrayObject {
   public function addAll($collection) {
-    foreach($collection as $element) {
-      $this->append($element);
-    }
+    foreach($collection as $element)
+      $this->add($element);
+
     return $collection;
   }
 
 
+  public function add($anObject) {
+    $this->append($anObject);
+    return $this;
+  }
+
+
   public function newInstance($elements) {
     $classname = get_class($this);
     return new $classname($elements);
   }
 
+
   public function collect($closure) {
     return $this->newInstance(array_map($closure,
                                         (array)$this));
diff --git a/src/Storm/Event/Save.php b/src/Storm/Event/Save.php
new file mode 100644
index 0000000000000000000000000000000000000000..2a1267a86f257c2985ceb9313055058057e656e7
--- /dev/null
+++ b/src/Storm/Event/Save.php
@@ -0,0 +1,39 @@
+<?php
+/*
+STORM is under the MIT License (MIT)
+
+Copyright (c) 2010-2020 Agence Française Informatique http://www.afi-sa.fr
+
+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, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+
+class Storm_Event_Save {
+  protected $_model;
+
+  public function __construct($model) {
+    $this->_model = $model;
+  }
+
+
+  public function getModel() {
+    return $this->_model;
+  }
+}
diff --git a/src/Storm/Events.php b/src/Storm/Events.php
new file mode 100644
index 0000000000000000000000000000000000000000..f497336bfc72e62536dc12a2cb6a88011b851be4
--- /dev/null
+++ b/src/Storm/Events.php
@@ -0,0 +1,98 @@
+<?php
+/*
+STORM is under the MIT License (MIT)
+
+Copyright (c) 2010-2020 Agence Française Informatique http://www.afi-sa.fr
+
+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, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+class Storm_Events {
+  protected static $_instance;
+
+  protected $_observers;
+
+  /** @category testing */
+  public static function setInstance($instance) {
+    static::$_instance = $instance;
+  }
+
+
+  public static function getInstance() {
+    return static::$_instance
+      ? static::$_instance
+      : static::$_instance = new static();
+  }
+
+
+  public function __construct() {
+    $this->_observers = new Storm_Set();
+  }
+
+
+  /**
+   * @param $observer callable
+   */
+  public function register($observer) {
+    $this->_observers->add($observer);
+    return $this;
+  }
+
+
+  /**
+   * @param $observer callable
+   */
+  public function unregister($observer) {
+    $this->_observers = $this->_observers
+      ->reject(function($each) use($observer)
+               {
+                 return $each === $observer;
+               });
+
+    return $this;
+  }
+
+
+  /**
+   * @param $classname name of a class of observer
+   */
+  public function unregisterClass($classname) {
+    $this->_observers = $this->_observers
+      ->reject(function($each) use($classname)
+               {
+                 return is_object($each) && get_class($each) == $classname;
+               });
+
+    return $this;
+  }
+
+
+  /**
+   * @param $event Storm_Event
+   */
+  public function notify($event) {
+    $this->_observers->eachDo(function($observer) use($event)
+                              {
+                                $observer($event);
+                              });
+
+    return $this;
+  }
+}
diff --git a/src/Storm/Model/Abstract.php b/src/Storm/Model/Abstract.php
index 9744b107f8163a9729f5aea00705e81cc92dcc1f..7e03c01f94d1a72b17f2dc70a74044c8bc56751f 100644
--- a/src/Storm/Model/Abstract.php
+++ b/src/Storm/Model/Abstract.php
@@ -268,13 +268,19 @@ abstract class Storm_Model_Abstract {
     $this->_updateNullBelongsToIdFieldsFromDependents();
     if ($valid = $this->isValid()) {
       $this->saveWithoutValidation();
-      $this->_attributes_in_db=$this->_attributes;
+      $this->_getEvents()->notify(new Storm_Event_Save($this));
+      $this->_attributes_in_db = $this->_attributes;
     }
 
     return $valid;
   }
 
 
+  protected function _getEvents() {
+    return Storm_Events::getInstance();
+  }
+
+
   public function assertSave() {
     if (!$this->save())
       throw new Storm_Model_Exception('Can\'t save '.get_class($this).': '.implode(',',$this->getErrors()));
diff --git a/src/Storm/Set.php b/src/Storm/Set.php
new file mode 100644
index 0000000000000000000000000000000000000000..d3337d618e6ea42de3293c9818f47264eba120da
--- /dev/null
+++ b/src/Storm/Set.php
@@ -0,0 +1,34 @@
+<?php
+/*
+STORM is under the MIT License (MIT)
+
+Copyright (c) 2010-2020 Agence Française Informatique http://www.afi-sa.fr
+
+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, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+class Storm_Set extends Storm_Collection {
+  public function add($anObject) {
+    if (!$this->includes($anObject))
+      parent::add($anObject);
+
+    return $this;
+  }
+}
diff --git a/tests/Storm/Model/EventTriggeringTest.php b/tests/Storm/Model/EventTriggeringTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..046ab262a351636f33698b477f74e981b7826f36
--- /dev/null
+++ b/tests/Storm/Model/EventTriggeringTest.php
@@ -0,0 +1,134 @@
+<?php
+/*
+STORM is under the MIT License (MIT)
+
+Copyright (c) 2010-2020 Agence Française Informatique http://www.afi-sa.fr
+
+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, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+
+abstract class Storm_Model_EventTriggeringTestCase extends Storm_Test_ModelTestCase {
+  protected
+    $_events,
+    $_callable,
+    $_called = false;
+
+  public function setUp() {
+    parent::setUp();
+    Storm_Model_Loader::defaultToVolatile();
+    $this->_events = Storm_Events::getInstance();
+  }
+
+
+  public function tearDown() {
+    Storm_Model_Loader::defaultToDb();
+    parent::tearDown();
+  }
+
+
+  /** @test */
+  public function withRegisteredClosureShouldCallBackOnSave() {
+    $this->_events->register($this->_callable);
+    (new Storm_Model_EventTriggeringModel)->save();
+    $this->assertTrue($this->_called);
+  }
+
+
+  /** @test */
+  public function withUnregisteredClosureShouldNotCallbackOnSave() {
+    $this->_events->register($this->_callable)
+                  ->unregister($this->_callable);
+    (new Storm_Model_EventTriggeringModel)->save();
+
+    $this->assertFalse($this->_called);
+  }
+}
+
+
+
+
+class Storm_Model_EventTriggeringClosureTest extends Storm_Model_EventTriggeringTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->_callable = function($event) { $this->_called = true; };
+  }
+}
+
+
+
+
+class Storm_Model_EventTriggeringCallableArrayTest extends Storm_Model_EventTriggeringTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->_callable = [$this, 'onSave'];
+  }
+
+
+  public function onSave($event) {
+    $this->_called = true;
+  }
+}
+
+
+
+
+class Storm_Model_EventTriggeringInvokableTest extends Storm_Model_EventTriggeringTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->_callable = new Storm_Model_EventTriggeringInvokable($this);
+  }
+
+
+  public function beCalled() {
+    $this->_called = true;
+  }
+
+
+  /** @test */
+  public function withUnregisteredByClassNameShouldNotCallbackOnSave() {
+    $this->_events->register($this->_callable)
+                  ->unregisterClass(Storm_Model_EventTriggeringInvokable::class);
+    (new Storm_Model_EventTriggeringModel)->save();
+
+    $this->assertFalse($this->_called);
+  }
+}
+
+
+
+
+class Storm_Model_EventTriggeringModel extends Storm_Model_Abstract {}
+
+
+
+
+class Storm_Model_EventTriggeringInvokable {
+  protected $_test;
+
+  public function __construct($test) {
+    $this->_test = $test;
+  }
+
+
+  public function __invoke($event) {
+    $this->_test->beCalled();
+  }
+}
\ No newline at end of file