diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,72 @@
+# hasql-dynamic-statements
+
+[![Hackage](https://img.shields.io/hackage/v/hasql-dynamic-statements.svg)](https://hackage.haskell.org/package/hasql-dynamic-statements)
+[![Continuous Haddock](https://img.shields.io/badge/haddock-master-blue)](https://nikita-volkov.github.io/hasql-dynamic-statements/)
+
+Hasql extension for composable, dynamic construction of SQL statements.
+
+## Overview
+
+This library provides a **Snippet** API that simplifies building SQL statements where the structure depends on runtime parameters. It abstracts over placeholder management and encoder matching, eliminating boilerplate and reducing bugs.
+
+## Key Features
+
+- **Composable**: Build statements using `Semigroup`/`Monoid` operations
+- **Type-safe**: Automatic parameter encoding with type-driven encoder derivation
+- **Dynamic**: Construct SQL conditionally based on parameters
+- **Clean API**: No manual placeholder numbering or encoder alignment
+
+## Quick Example
+
+```haskell
+import Hasql.DynamicStatements.Snippet
+
+-- Build a dynamic substring query
+selectSubstring :: Text -> Maybe Int32 -> Maybe Int32 -> Snippet
+selectSubstring string from to =
+  "select substring(" <> param string <>
+  foldMap (\x -> " from " <> param x) from <>
+  foldMap (\x -> " for " <> param x) to <>
+  ")"
+
+-- Execute in a session
+result <- toSession (selectSubstring "hello" (Just 2) Nothing) 
+  (singleRow $ column $ nonNullable text)
+```
+
+## Without Snippet API
+
+Compare the above to manual construction:
+
+```haskell
+selectSubstring' :: Text -> Maybe Int32 -> Maybe Int32 -> Statement () Text
+selectSubstring' string from to = 
+  let sql = case (from, to) of
+        (Just _, Just _)  -> "select substring($1 from $2 to $3)"
+        (Just _, Nothing) -> "select substring($1 from $2)"
+        (Nothing, Just _) -> "select substring($1 to $2)"
+        (Nothing, Nothing) -> "select substring($1)"
+      encoder = 
+        Encoders.param (string >$ Encoders.text) <>
+        foldMap (\x -> Encoders.param (x >$ Encoders.int8)) from <>
+        foldMap (\x -> Encoders.param (x >$ Encoders.int8)) to
+      decoder = singleRow $ column $ nonNullable text
+  in Statement.preparable sql encoder decoder
+```
+
+The Snippet API eliminates placeholder numbering, pattern matching on parameter presence, and manual encoder sequencing.
+
+## Core API
+
+### Construction
+
+- **`sql`** - Raw SQL text
+- **`param`** - Parameter with implicit encoder
+- **`encoderAndParam`** - Parameter with explicit encoder
+
+### Execution
+
+- **`toSql`** - Compile to SQL text with placeholders
+- **`toStatement`** - Create a `Statement`
+- **`toSession`** - Execute directly in `Session`
+- **`toPipeline`** - Execute in `Pipeline`
diff --git a/hasql-dynamic-statements.cabal b/hasql-dynamic-statements.cabal
--- a/hasql-dynamic-statements.cabal
+++ b/hasql-dynamic-statements.cabal
@@ -1,10 +1,31 @@
 cabal-version: 3.0
 name: hasql-dynamic-statements
-version: 0.5
+version: 0.5.0.1
 synopsis: Hasql extension for dynamic construction of statements
 description:
-  Library extending Hasql with an abstraction that simplifies construction of SQL statements, the structure of which depends on the parameters.
+  Library extending Hasql with a composable API for building SQL statements dynamically.
 
+  The core abstraction is 'Snippet', which allows you to construct SQL statements with parameters
+  injected in-place, automatically handling placeholders and encoder matching. This is particularly
+  useful when the structure of your SQL depends on runtime parameters.
+
+  Key features:
+
+  * Composable SQL snippets using 'Semigroup' and 'Monoid' instances
+  * Automatic placeholder generation and parameter encoding
+  * Type-safe parameter injection with implicit encoder derivation
+  * Protection against common SQL construction bugs
+  * Support for conditional SQL structure based on parameters
+
+  Example:
+
+  > selectSubstring :: Text -> Maybe Int32 -> Maybe Int32 -> Snippet
+  > selectSubstring string from to =
+  >   "select substring(" <> param string <>
+  >   foldMap (\x -> " from " <> param x) from <>
+  >   foldMap (\x -> " for " <> param x) to <>
+  >   ")"
+
 homepage: https://github.com/nikita-volkov/hasql-dynamic-statements
 bug-reports:
   https://github.com/nikita-volkov/hasql-dynamic-statements/issues
@@ -16,6 +37,7 @@
 license-file: LICENSE
 extra-doc-files:
   CHANGELOG.md
+  README.md
 
 source-repository head
   type: git
diff --git a/src/library/Hasql/DynamicStatements/Snippet.hs b/src/library/Hasql/DynamicStatements/Snippet.hs
--- a/src/library/Hasql/DynamicStatements/Snippet.hs
+++ b/src/library/Hasql/DynamicStatements/Snippet.hs
@@ -45,23 +45,29 @@
 -- 'Hasql.DynamicStatements.Snippet.toStatement' or directly execute it in
 -- 'Hasql.Session.Session' using
 -- 'Hasql.DynamicStatements.Snippet.toSession'.
-newtype Snippet = Snippet (Seq SnippetChunk)
-
-data SnippetChunk
-  = StringSnippetChunk TextBuilder.TextBuilder
-  | ParamSnippetChunk (Encoders.Params ())
+data Snippet
+  = Snippet
+      (Int -> TextBuilder.TextBuilder)
+      Int
+      (Encoders.Params ())
 
-deriving instance Semigroup Snippet
+instance Semigroup Snippet where
+  Snippet sql1 paramCount1 encoder1 <> Snippet sql2 paramCount2 encoder2 =
+    Snippet
+      (\paramId -> sql1 paramId <> sql2 (paramId + paramCount1))
+      (paramCount1 + paramCount2)
+      (encoder1 <> encoder2)
 
-deriving instance Monoid Snippet
+instance Monoid Snippet where
+  mempty = Snippet (\_ -> mempty) 0 mempty
 
 instance IsString Snippet where
-  fromString x = Snippet (pure (StringSnippetChunk (fromString x)))
+  fromString = sql . fromString
 
 -- |
 -- SQL chunk.
 sql :: Text -> Snippet
-sql x = Snippet (pure (StringSnippetChunk (TextBuilder.text x)))
+sql x = Snippet (const (TextBuilder.text x)) 0 mempty
 
 -- |
 -- Parameter encoded using an implicitly derived encoder from the type.
@@ -71,26 +77,17 @@
 -- |
 -- Parameter with an explicitly defined encoder.
 encoderAndParam :: Encoders.NullableOrNot Encoders.Value param -> param -> Snippet
-encoderAndParam encoder param = Snippet (pure (ParamSnippetChunk (param >$ Encoders.param encoder)))
-
-compile :: Snippet -> (Text, Encoders.Params ())
-compile (Snippet chunks) =
-  let step (!paramId, !builder, !encoder) = \case
-        StringSnippetChunk sql ->
-          (paramId, builder <> sql, encoder)
-        ParamSnippetChunk paramEncoder ->
-          let newParamId = paramId + 1
-              newPoking = builder <> "$" <> TextBuilder.decimal paramId
-              newEncoder = encoder <> paramEncoder
-           in (newParamId, newPoking, newEncoder)
-      (_, builder, encoder) = foldl' step (1 :: Int, mempty, mempty) chunks
-      sql = TextBuilder.toText builder
-   in (sql, encoder)
+encoderAndParam encoder param =
+  Snippet
+    (\paramId -> "$" <> TextBuilder.decimal paramId)
+    1
+    (param >$ Encoders.param encoder)
 
 -- |
 -- Compile a snippet into SQL text with placeholders.
 toSql :: Snippet -> Text
-toSql snippet = fst (compile snippet)
+toSql (Snippet sql _ _) =
+  TextBuilder.toText (sql 1)
 
 -- |
 -- Construct a statement dynamically, specifying the parameters in-place
@@ -136,9 +133,8 @@
 -- As you can see, the Snippet API abstracts over placeholders and
 -- matching encoder generation, thus also protecting you from all sorts of related bugs.
 toStatement :: Snippet -> Decoders.Result result -> Statement.Statement () result
-toStatement snippet =
-  let (sql, encoder) = compile snippet
-   in Statement.unpreparable sql encoder
+toStatement (Snippet sql _ encoder) =
+  Statement.unpreparable (TextBuilder.toText (sql 1)) encoder
 
 -- |
 -- Execute in @Session.Session@, providing a result decoder.
