diff --git a/CSV Metrics Logger/CSVStorage.cs b/CSV Metrics Logger/CSVStorage.cs
new file mode 100644
index 0000000..24b8f7b
--- /dev/null
+++ b/CSV Metrics Logger/CSVStorage.cs	
@@ -0,0 +1,164 @@
+using System.Collections.Concurrent;
+using System.Text;
+
+// ReSharper disable StaticMemberInGenericType
+
+namespace CSV_Metrics_Logger;
+
+/// <summary>
+/// The CSV storage class.
+/// </summary>
+public sealed class CSVStorage<T> : IAsyncDisposable where T : struct, IConvertToCSV
+{
+    private const char CSV_DEFAULT_DELIMITER = ';';
+    
+    private static readonly TimeSpan WRITE_INTERVAL = TimeSpan.FromSeconds(8);
+
+    private readonly object lockObject = new();
+    private readonly FileStream fileStream;
+    private readonly TextWriter writer;
+    private readonly char csvDelimiter;
+    private readonly ConcurrentQueue<string> dataQueue = new();
+    private readonly PeriodicTimer writeTimer = new(WRITE_INTERVAL);
+    
+    private bool isInitialized;
+    private bool writerRunning;
+    
+    private CSVStorage(string filename, char csvDelimiter)
+    {
+        this.csvDelimiter = csvDelimiter;
+        this.isInitialized = File.Exists(filename) && new FileInfo(filename).Length > 0;
+        this.fileStream = new FileStream(filename, FileMode.Append, FileAccess.Write, FileShare.Read);
+        this.writer = new StreamWriter(this.fileStream, Encoding.UTF8);
+
+        _ = Task.Factory.StartNew(() => this.WriteDataWorker(), TaskCreationOptions.LongRunning);
+    }
+
+    /// <summary>
+    /// Create a new CSV storage instance for the given type T and filename.
+    /// </summary>
+    /// <remarks>
+    /// When the file already exists, added data will be appended to the file.
+    /// The header will be written only once.
+    /// </remarks>
+    /// <param name="filename">The filename to store the CSV data.</param>
+    /// <param name="csvDelimiter">The CSV delimiter to use. Default is a semicolon.</param>
+    /// <returns>The CSV storage instance.</returns>
+    public static CSVStorage<T> Create(string filename, char csvDelimiter = CSV_DEFAULT_DELIMITER) => new(filename, csvDelimiter);
+    
+    /// <summary>
+    /// Write the given data to the CSV file.
+    /// </summary>
+    /// <param name="data">The data to write.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    public void Write(T data, CancellationToken cancellationToken = default)
+    {
+        if(!this.isInitialized)
+        {
+            lock (this.lockObject)
+            {
+                if(!this.isInitialized)
+                {
+                    this.dataQueue.Enqueue(this.CreateCSVLine(data.GetCSVHeaders(), cancellationToken));
+                    this.isInitialized = true;
+                }
+            }
+        }
+        
+        this.dataQueue.Enqueue(this.CreateCSVLine(data.ConvertToCSVDataLine(), cancellationToken));
+    }
+
+    private async Task WriteDataWorker(CancellationToken cancellationToken = default)
+    {
+        this.writerRunning = true;
+        while (await this.writeTimer.WaitForNextTickAsync(cancellationToken))
+            await this.WriteData(cancellationToken);
+        
+        this.writerRunning = false;
+    }
+    
+    private async ValueTask WriteData(CancellationToken cancellationToken = default)
+    {
+        if(this.dataQueue.IsEmpty)
+            return;
+    
+        while(this.dataQueue.TryDequeue(out var line))
+        {
+            if(cancellationToken.IsCancellationRequested)
+                break;
+            
+            await this.writer.WriteLineAsync(line);
+        }
+    }
+
+    private string CreateCSVLine(IEnumerable<string> elements, CancellationToken cancellationToken = default)
+    {
+        var sb = new StringBuilder(1_024);
+        foreach (var element in elements)
+        {
+            if(cancellationToken.IsCancellationRequested)
+                break;
+            
+            if (sb.Length > 0)
+                sb.Append(this.csvDelimiter);
+
+            if (this.QuotationNeeded(element))
+            {
+                sb.Append('"');
+                sb.Append(element.Replace("\"", "\"\""));
+                sb.Append('"');
+            }
+            else
+                sb.Append(element);
+        }
+
+        return sb.ToString();
+    }
+
+    private bool QuotationNeeded(string value)
+    {
+        // Rules:
+        // - If the value contains a comma, we need to use double quotes.
+        // - If the value contains the delimiter, we need to use double quotes.
+        // - If the value contains the escape character, we need to use double quotes.
+        // - If the value contains a newline character, we need to use double quotes.
+        // - If the value contains a double quote, we need to escape it by doubling it.
+        // - If the value contains spaces, we need to use double quotes.
+        //
+        if (value.Contains(',') ||
+            value.Contains(this.csvDelimiter) ||
+            value.Contains('"') ||
+            value.Contains('\n') ||
+            value.Contains(' '))
+        {
+            return true;
+        }
+
+        return false;
+    }
+
+    #region Implementation of IAsyncDisposable
+
+    /// <inheritdoc />
+    public async ValueTask DisposeAsync()
+    {
+        // Stop the writer timer:
+        this.writeTimer.Dispose();
+        
+        // Now, we wait for the last scheduled write operation to finish, if any:
+        while (this.writerRunning)
+            await Task.Delay(TimeSpan.FromMilliseconds(100));
+     
+        // Ensure that all data is written:
+        await this.WriteData();
+        
+        // Flush and dispose the writer:
+        await this.writer.FlushAsync();
+        await this.writer.DisposeAsync();
+        
+        // Dispose the file stream:
+        await this.fileStream.DisposeAsync();
+    }
+
+    #endregion
+}
\ No newline at end of file
diff --git a/Tests/Tests.cs b/Tests/Tests.cs
index ce84396..ee0d656 100644
--- a/Tests/Tests.cs
+++ b/Tests/Tests.cs
@@ -1,4 +1,5 @@
 using System.Diagnostics.CodeAnalysis;
+using CSV_Metrics_Logger;
 
 namespace Tests;
 
@@ -81,4 +82,39 @@ public sealed class Tests
             Assert.That(dataLine, Is.EquivalentTo(new[] { "New York", "35", "1.85" }));
         });
     }
+
+    [Test]
+    public async Task TestWritingCSV()
+    {
+        List<TestDataOneLine> testData =
+        [
+            new TestDataOneLine("Name 1", 14),
+            new TestDataOneLine("Name 2", 25),
+            new TestDataOneLine("Name 3", 36),
+            new TestDataOneLine("Name 4", 47),
+            new TestDataOneLine("Name 5", 58),
+        ];
+        
+        // Get a random file:
+        var fileName = Path.GetTempFileName();
+        await using (var storage = CSVStorage<TestDataOneLine>.Create(fileName))
+        {
+            foreach (var data in testData)
+                storage.Write(data);
+        }
+        
+        var lines = await File.ReadAllLinesAsync(fileName);
+        Assert.Multiple(() =>
+        {
+            Assert.That(lines, Has.Length.EqualTo(6));
+            Assert.That(lines[0], Is.EqualTo("Name;Age"));
+            Assert.That(lines[1], Is.EqualTo("\"Name 1\";14"));
+            Assert.That(lines[2], Is.EqualTo("\"Name 2\";25"));
+            Assert.That(lines[3], Is.EqualTo("\"Name 3\";36"));
+            Assert.That(lines[4], Is.EqualTo("\"Name 4\";47"));
+            Assert.That(lines[5], Is.EqualTo("\"Name 5\";58"));
+        });
+        
+        File.Delete(fileName);
+    }
 }
\ No newline at end of file