diff --git a/composer.json b/composer.json
index 97a09de..9ffc09e 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,12 @@
   "require": {
     "php": ">=7.2",
     "fastforward/data-structures": "^1.0.0",
-    "fastforward/factories": "^1.2.0"
+    "fastforward/factories": "^1.2.0",
+    "symfony/config": "~4.3",
+    "symfony/http-foundation": "~4.3",
+    "symfony/routing": "~4.3",
+    "symfony/yaml": "~4.3",
+    "twig/twig": "^2.0"
   },
   "require-dev": {
     "phpunit/phpunit": "^8"
diff --git a/src/Events/Templating/PostRender.php b/src/Events/Templating/PostRender.php
new file mode 100644
index 0000000..d2bbd74
--- /dev/null
+++ b/src/Events/Templating/PostRender.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * Definition of PostRender
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @copyright 2019-forever Marco Stoll
+ * @filesource
+ */
+declare(strict_types=1);
+
+namespace FF\Events\Templating;
+
+use FF\Events\AbstractEvent;
+use FF\Services\Templating\RenderedDocument;
+
+/**
+ * Class PostRender
+ *
+ * @package FF\Events\Templating
+ */
+class PostRender extends AbstractEvent
+{
+    /**
+     * @var RenderedDocument
+     */
+    protected $doc;
+
+    /**
+     * @param RenderedDocument $doc
+     */
+    public function __construct(RenderedDocument $doc)
+    {
+        $this->doc = $doc;
+    }
+
+    /**
+     * @return RenderedDocument
+     */
+    public function getDoc(): RenderedDocument
+    {
+        return $this->doc;
+    }
+}
\ No newline at end of file
diff --git a/src/Events/Templating/PreRender.php b/src/Events/Templating/PreRender.php
new file mode 100644
index 0000000..d868b46
--- /dev/null
+++ b/src/Events/Templating/PreRender.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * Definition of PreRender
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @copyright 2019-forever Marco Stoll
+ * @filesource
+ */
+declare(strict_types=1);
+
+namespace FF\Events\Templating;
+
+use FF\DataStructures\Record;
+use FF\Events\AbstractEvent;
+
+/**
+ * Class PreForward
+ *
+ * @package FF\Events\Templating
+ */
+class PreRender extends AbstractEvent
+{
+    /**
+     * @var string
+     */
+    protected $template;
+
+    /**
+     * @var Record
+     */
+    protected $data;
+
+    /**
+     * @param string $template
+     * @param Record $data
+     */
+    public function __construct(string $template, Record $data)
+    {
+        $this->template = $template;
+        $this->data = $data;
+    }
+
+    /**
+     * @return string
+     */
+    public function getTemplate(): string
+    {
+        return $this->template;
+    }
+
+    /**
+     * @return Record
+     */
+    public function getData(): Record
+    {
+        return $this->data;
+    }
+}
\ No newline at end of file
diff --git a/src/Services/Templating/Exceptions/RenderingException.php b/src/Services/Templating/Exceptions/RenderingException.php
new file mode 100644
index 0000000..b25b15b
--- /dev/null
+++ b/src/Services/Templating/Exceptions/RenderingException.php
@@ -0,0 +1,21 @@
+<?php
+/**
+* Definition of RenderingException
+*
+* @author Marco Stoll <marco@fast-forward-encoding.de>
+* @copyright 2019-forever Marco Stoll
+* @filesource
+*/
+declare(strict_types=1);
+
+namespace FF\Services\Templating\Exceptions;
+
+/**
+* Class RenderingException
+*
+* @package FF\Services\Templating\Exceptions
+*/
+class RenderingException extends \RuntimeException
+{
+
+}
\ No newline at end of file
diff --git a/src/Services/Templating/RenderedDocument.php b/src/Services/Templating/RenderedDocument.php
new file mode 100644
index 0000000..0e84bf0
--- /dev/null
+++ b/src/Services/Templating/RenderedDocument.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Definition of RenderedDocument
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @copyright 2019-forever Marco Stoll
+ * @filesource
+ */
+declare(strict_types=1);
+
+namespace FF\Services\Templating;
+
+/**
+ * Class RenderedDocument
+ *
+ * @package FF\Services\Templating
+ */
+class RenderedDocument
+{
+    /**
+     * @var string
+     */
+    private $contents;
+
+    /**
+     * @param string $contents
+     */
+    public function __construct(string $contents)
+    {
+        $this->contents = $contents;
+    }
+
+    /**
+     * @return string
+     */
+    public function getContents(): string
+    {
+        return $this->contents;
+    }
+
+    /**
+     * @param string $contents
+     * @return $this
+     */
+    public function setContents(string $contents)
+    {
+        $this->contents = $contents;
+        return $this;
+    }
+}
diff --git a/src/Services/Templating/TemplateRendererInterface.php b/src/Services/Templating/TemplateRendererInterface.php
new file mode 100644
index 0000000..493dcca
--- /dev/null
+++ b/src/Services/Templating/TemplateRendererInterface.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Definition of TemplateRendererInterface
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @copyright 2019-forever Marco Stoll
+ * @filesource
+ */
+declare(strict_types=1);
+
+namespace FF\Services\Templating;
+
+use FF\Services\Templating\Exceptions\RenderingException;
+
+/**
+ * Interface TemplateRendererInterface
+ *
+ * @package FF\Services\Templating
+ */
+interface TemplateRendererInterface
+{
+    /**
+     * Renders a template using the given data
+     *
+     * @param string $template
+     * @param array $data
+     * @return string
+     * @throws RenderingException
+     * @fires Templating\PreRender
+     * @fires Templating\PostRender
+     */
+    public function render(string $template, array $data): string;
+}
diff --git a/src/Services/Templating/TwigRenderer.php b/src/Services/Templating/TwigRenderer.php
new file mode 100644
index 0000000..ada0534
--- /dev/null
+++ b/src/Services/Templating/TwigRenderer.php
@@ -0,0 +1,186 @@
+<?php
+/**
+ * Definition of TwigRenderer
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @copyright 2019-forever Marco Stoll
+ * @filesource
+ */
+
+declare(strict_types=1);
+
+namespace FF\Services\Templating;
+
+use FF\DataStructures\Record;
+use FF\Services\AbstractService;
+use FF\Services\Templating\Exceptions\RenderingException;
+use Twig\Environment;
+use Twig\Error\Error;
+use Twig\Extension\ExtensionInterface;
+use Twig\Loader\FilesystemLoader;
+use Twig\NodeVisitor\NodeVisitorInterface;
+use Twig\RuntimeLoader\RuntimeLoaderInterface;
+use Twig\TokenParser\TokenParserInterface;
+use Twig\TwigFilter;
+use Twig\TwigFunction;
+use Twig\TwigTest;
+
+/**
+ * Class TwigRenderer
+ *
+ * Options:
+ *
+ *  - twig-options  : array (default: [])       - options to configure twig environment
+ *  - template-dir  : string                    - path to templates folder
+ *  - fire-events   : bool (default: false)     - whether to fire rendering events
+ *
+ * @method void addRuntimeLoader(RuntimeLoaderInterface $loader)
+ * @method void addExtension(ExtensionInterface $extension)
+ * @method void addTokenParser(TokenParserInterface $parser)
+ * @method void addNodeVisitor(NodeVisitorInterface $visitor)
+ * @method void addFilter(TwigFilter $filter)
+ * @method void addTest(TwigTest $test)
+ * @method void addFunction(TwigFunction $function)
+ * @method void addGlobal(string $name, mixed $value)
+ *
+ * @package FF\Services\Templating
+ *
+ * @link https://twig.symfony.com/doc/2.x/api.html#environment-options Twig Environment options
+ * @link https://twig.symfony.com/doc/2.x/advanced.html Extending Twig
+ */
+class TwigRenderer extends AbstractService implements TemplateRendererInterface
+{
+    /**
+     * @var Environment
+     */
+    protected $twig;
+
+    /**
+     * @var bool
+     */
+    protected $fireEvents;
+
+    /**
+     * @return Environment
+     */
+    public function getTwig(): Environment
+    {
+        return $this->twig;
+    }
+
+    /**
+     * @param Environment $twig
+     * @return $this
+     */
+    public function setTwig(Environment $twig)
+    {
+        $this->twig = $twig;
+        return $this;
+    }
+
+    /**
+     * @return bool
+     */
+    public function getFireEvents(): bool
+    {
+        return $this->fireEvents;
+    }
+
+    /**
+     * @param bool $fireEvents
+     * @return $this
+     */
+    public function setFireEvents(bool $fireEvents)
+    {
+        $this->fireEvents = $fireEvents;
+        return $this;
+    }
+
+    /**
+     * Renders a template using the given data
+     *
+     * @param string $template
+     * @param array $data
+     * @return string
+     * @throws RenderingException
+     * @fires Templating\PreRender
+     * @fires Templating\PostRender
+     */
+    public function render(string $template, array $data): string
+    {
+        if ($this->fireEvents) {
+            // wrap data in object to support data manipulation via event listener
+            $record = new Record($data);
+            $this->fire('Templating\PreRender', $template, $record);
+            $data = $record->getDataAsArray();
+        }
+
+        try {
+            $contents = $this->twig->render($template, $data);
+        } catch (Error $e) {
+            throw new RenderingException($e->getMessage(), $e->getCode(), $e);
+        }
+
+        if ($this->fireEvents) {
+            // wrap data in object to support data manipulation via event listener
+            $doc = new RenderedDocument($contents);
+            $this->fire('Templating\PostRender', $doc);
+            $contents = $doc->getContents();
+        }
+
+        return $contents;
+    }
+
+    /**
+     * Magic proxy for the public api of Twig\Environment
+     *
+     * Routes the method call to the twig environment instance encapsulated
+     * within this service.
+     *
+     * @param string $name
+     * @param array $arguments
+     * @return mixed
+     * @throws \BadMethodCallException Method is not defined or accessible on Twig\Environment
+     */
+    public function __call(string $name, array $arguments = [])
+    {
+        $callable = [$this->twig, $name];
+        if (!is_callable($callable)) {
+            // trigger fatal error: unsupported method call
+            // mimic standard php error message
+            // Fatal error: Call to undefined method {class}::{method}() in {file} on line {line}
+            $backTrace = debug_backtrace();
+            $errorMsg = 'Call to undefined method ' . __CLASS__ . '::' . $name . '() '
+                . 'in ' . $backTrace[0]['file'] . ' on line ' . $backTrace[0]['line'];
+            trigger_error($errorMsg, E_USER_ERROR);
+        }
+
+        return call_user_func_array($callable, $arguments);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function initialize(array $options)
+    {
+        parent::initialize($options);
+
+        $this->fireEvents = $this->getOption('fire-events', false);
+
+        $loader = new FilesystemLoader($this->getOption('template-dir'));
+        $this->twig = new Environment($loader, $this->getOption('twig-options', []));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function validateOptions(array $options, array &$errors): bool
+    {
+        if (!isset($options['template-dir']) || empty($options['template-dir'])) {
+            $errors[] = 'missing or empty mandatory option [template-dir]';
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/tests/Services/Templating/TwigRendererTest.php b/tests/Services/Templating/TwigRendererTest.php
new file mode 100644
index 0000000..5d6e378
--- /dev/null
+++ b/tests/Services/Templating/TwigRendererTest.php
@@ -0,0 +1,181 @@
+<?php
+/**
+ * Definition of TwigRendererTest
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @copyright 2019-forever Marco Stoll
+ * @filesource
+ */
+declare(strict_types=1);
+
+namespace FF\Tests\Services\Templating;
+
+use FF\Events\AbstractEvent;
+use FF\Events\Templating\PostRender;
+use FF\Events\Templating\PreRender;
+use FF\Factories\ServicesFactory;
+use FF\Factories\SF;
+use FF\Services\Events\EventBroker;
+use FF\Services\Exceptions\ConfigurationException;
+use FF\Services\Templating\Exceptions\RenderingException;
+use FF\Services\Templating\TwigRenderer;
+use PHPUnit\Framework\Error\Error;
+use PHPUnit\Framework\TestCase;
+use Twig\Environment;
+use Twig\Loader\ArrayLoader;
+
+/**
+ * Test TwigRendererTest
+ *
+ * @package FF\Tests
+ */
+class TwigRendererTest extends TestCase
+{
+    const DEFAULT_OPTIONS = [
+        'template-dir' => __DIR__ . '/templates'
+    ];
+
+    /**
+     * @var TwigRenderer
+     */
+    protected $uut;
+
+    /**
+     * @var AbstractEvent[]
+     */
+    protected static $lastEvents;
+
+    /**
+     * {@inheritdoc}
+     */
+    public static function setUpBeforeClass(): void
+    {
+        SF::setInstance(new ServicesFactory());
+
+        /** @var EventBroker $eventBroker */
+        $eventBroker = SF::i()->get('Events\EventBroker');
+
+        // register test listener
+        $eventBroker
+            ->subscribe([__CLASS__, 'listener'], 'Templating\PreRender')
+            ->subscribe([__CLASS__, 'listener'], 'Templating\PostRender');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function setUp(): void
+    {
+        $this->uut = new TwigRenderer(self::DEFAULT_OPTIONS);
+
+        self::$lastEvents = [];
+    }
+
+    /**
+     * Dummy event listener
+     *
+     * @param AbstractEvent $event
+     */
+    public static function listener(AbstractEvent $event)
+    {
+        self::$lastEvents[get_class($event)] = $event;
+    }
+
+    /**
+     * Tests the namesake method/feature
+     */
+    public function testInitializeErrorMandatoryOptions()
+    {
+        $this->expectException(ConfigurationException::class);
+
+        new TwigRenderer();
+    }
+
+    /**
+     * Tests the namesake method/feature
+     */
+    public function testSetGetTwig()
+    {
+        $value = new Environment(new ArrayLoader());
+        $same = $this->uut->setTwig($value);
+        $this->assertSame($this->uut, $same);
+        $this->assertEquals($value, $this->uut->getTwig());
+    }
+
+    /**
+     * Tests the namesake method/feature
+     */
+    public function testSetGetFireEvents()
+    {
+        $value = false;
+        $same = $this->uut->setFireEvents($value);
+        $this->assertSame($this->uut, $same);
+        $this->assertEquals($value, $this->uut->getFireEvents());
+    }
+
+    /**
+     * Tests the namesake method/feature
+     */
+    public function testRenderWithoutEvents()
+    {
+        $doc = $this->uut->setFireEvents(false)
+            ->render('basic.html.twig', ['foo' => 'bar']);
+        $this->assertEquals('foo: bar', $doc);
+
+        $this->assertEmpty(self::$lastEvents);
+    }
+
+    /**
+     * Tests the namesake method/feature
+     */
+    public function testRenderWithEvents()
+    {
+        $doc = $this->uut->setFireEvents(true)
+            ->render('basic.html.twig', ['foo' => 'bar']);
+        $this->assertEquals('foo: bar', $doc);
+
+        $this->assertArrayHasKey(PreRender::class, self::$lastEvents);
+        $this->assertArrayHasKey(PostRender::class, self::$lastEvents);
+    }
+
+    /**
+     * Tests the namesake method/feature
+     */
+    public function testRenderErrorLoader()
+    {
+        $this->expectException(RenderingException::class);
+
+        $this->uut->render('missing.html.twig', []);
+    }
+
+    /**
+     * Tests the namesake method/feature
+     */
+    public function testRenderErrorSyntax()
+    {
+        $this->expectException(RenderingException::class);
+
+        $this->uut->render('invalid.html.twig', []);
+    }
+
+    /**
+     * Tests the namesake method/feature
+     */
+    public function testMagicCall()
+    {
+        $this->uut->addGlobal('foo', 'bar');
+
+        $doc = $this->uut->render('basic.html.twig', []);
+        $this->assertEquals('foo: bar', $doc);
+    }
+
+    /**
+     * Tests the namesake method/feature
+     */
+    public function testMagicCallUnknown()
+    {
+        $this->expectException(Error::class);
+
+        $this->uut->foo();
+    }
+}
\ No newline at end of file
diff --git a/tests/Services/Templating/templates/basic.html.twig b/tests/Services/Templating/templates/basic.html.twig
new file mode 100644
index 0000000..13b0563
--- /dev/null
+++ b/tests/Services/Templating/templates/basic.html.twig
@@ -0,0 +1 @@
+foo: {{ foo }}
\ No newline at end of file
diff --git a/tests/Services/Templating/templates/invalid.html.twig b/tests/Services/Templating/templates/invalid.html.twig
new file mode 100644
index 0000000..a127b5a
--- /dev/null
+++ b/tests/Services/Templating/templates/invalid.html.twig
@@ -0,0 +1,2 @@
+{% for %}
+{% endif %}
\ No newline at end of file