Azure Cosmos DB introduction with F# by Andrii Chebukin

February 17, 2026

In his talk Azure Cosmos DB introduction with F#, Andrii Chebukin gave a practical overview of Azure Cosmos DB — what it is, how it works, where it fits, and how to make it easier to use from F#. The session combined distributed database fundamentals, real-world implementation lessons, and a look at a custom F# library Andrii built to simplify Cosmos DB interactions.

A comprehensive overview: Azure Cosmos DB introduction with F#

Why Cosmos DB?

Andrii’s perspective comes from hands-on production work: he has used Cosmos DB in both single-tenant and multi-tenant systems, and even explored it with Gremlin for graph-style data.

His key message was that Cosmos DB is especially useful in three scenarios:

  • Very high load, where you want guaranteed throughput
  • Multi-region applications, where you need data replicated and synchronized across geographies
  • Very low load, where a serverless model can make costs surprisingly small

In short, Cosmos DB shines when you need elastic scale, global distribution, or predictable performance.

 

CAP, PACELC, and Consistency Choices

The talk began with a quick foundation in distributed database theory:

  • CAP theorem: you can’t fully maximize consistency, availability, and partition tolerance at the same time
  • PACELC: in distributed systems, the tradeoff is often not just during partitions, but also between latency and consistency

Cosmos DB exposes this reality through five consistency levels, from strongest to weakest:

  • Strong
  • Bounded staleness
  • Session
  • Consistent prefix
  • Eventual

These let you tune the balance between correctness, latency, and cost. Strong consistency gives you the most guarantees, but also the highest cost. Eventual consistency gives you the most speed, but the fewest guarantees.

 

Cosmos DB as a Multi-Model Database

One of Cosmos DB’s strengths is that it supports several APIs:

  • NoSQL (Microsoft’s document API)
  • MongoDB
  • Cassandra
  • Gremlin
  • Azure Table
  • PostgreSQL (via Citus)

Although Andrii mainly uses the NoSQL API, he highlighted Gremlin as particularly interesting because it allows graph-style modeling on top of document storage.

Cosmos DB is structured in layers:

  • Account
  • Database
  • Container
  • Item

The container is roughly analogous to a table, but it can store very different shapes of data depending on the API you use.

 

Pricing: Throughput, Request Units, and Serverless

Cosmos DB pricing is driven mainly by two things:

  • Storage
  • Request Units (RUs)

A request unit is an abstraction over database work:

  • reading a 1 KB document ≈ 1 RU
  • writes cost more
  • queries cost more depending on complexity

You can provision Cosmos DB in two main ways:

Provisioned throughput

You reserve a fixed or autoscaling amount of throughput.

Serverless

You pay only for what you use, but with limitations such as:

  • single-region deployment
  • throughput caps per container

Andrii emphasized an important design principle:

Storage is cheaper than compute in Cosmos DB.

That means denormalization is often the right tradeoff. Copying data may increase storage use a little, but it can dramatically reduce query cost and RU consumption.

 

Partitioning: The Most Important Design Decision

If there was one recurring theme in the presentation, it was this:

Choosing the right partition key is critical.

Cosmos DB uses:

  • Logical partitions — defined by your partition key
  • Physical partitions — the actual servers holding the data

Your partition key determines:

  • how evenly data is distributed
  • how efficiently queries are routed
  • whether you get the performance you pay for

Bad partition keys include:

  • values with very low cardinality
  • sequential timestamps
  • values that funnel most requests into one hot partition

Better choices include:

  • tenant ID
  • user ID
  • device ID
  • hierarchical combinations of these

A newer and very useful feature is hierarchical partition keys, where you can define up to three levels. This helps avoid hot partitions while still supporting efficient routing for common query patterns.

And if you get it wrong?
You can repartition into a new container later — but it costs time and migration effort, so it’s worth getting right early.

 

Data Modeling: Denormalize Aggressively

Because compute is the expensive part of Cosmos DB, Andrii recommends optimizing for read and query efficiency, not relational purity.

That means:

  • duplicate data when it helps
  • avoid over-normalization
  • think in terms of documents and access patterns

He also pointed out that Cosmos DB now supports:

  • vector search
  • full-text search
  • secondary global indexes

These make Cosmos more capable as a modern application database — especially in AI-heavy or search-heavy workloads.

 

Working with Data

Andrii walked through several common operations:

Reading

You can:

  • read by id + partition key
  • read many items efficiently
  • query using SQL-like syntax
  • query with LINQ
  • paginate using continuation tokens

He also demonstrated cursor-based pagination patterns useful for GraphQL-style APIs.

Writing

You can:

  • create items
  • replace items
  • upsert
  • delete
  • delete a full partition

Patch operations

A powerful recent feature is JSON Patch, which lets you update parts of a document without rewriting the entire item.

This is especially useful when you only need to modify a handful of properties.

TTL (Time to Live)

Cosmos DB can automatically expire documents for free during idle capacity periods, making it great for temporary data or cleanup scenarios.

 

Modeling Multiple Types in One Container

A very practical technique Andrii showed is storing multiple document types in the same container using discriminators.

By combining:

  • a base entity
  • derived types
  • versioned discriminator fields
  • .NET 9’s JsonDerivedType

…you can deserialize a single container into multiple F# / .NET types cleanly.

He also showed how versioning can be handled incrementally using JSON converters, allowing schema evolution without requiring a full upfront migration.

This makes it possible to:

  • store related entities together
  • query base and derived types
  • evolve the data model gradually

 

F# Ergonomics: A Cosmos DB Library with Computation Expressions

One of the highlights of the talk was Andrii’s custom F# library for Cosmos DB.

The problem he wanted to solve was that the raw Cosmos SDK can feel verbose and imperative:

  • you build option objects
  • remember status codes
  • wire up request settings manually
  • handle many cases yourself

His F# approach wraps operations like:

  • create
  • read
  • replace
  • upsert
  • delete

…in computation expressions, so the code becomes more declarative and easier to reason about.

Instead of manually handling many low-level SDK details, you get:

  • a clearer operation definition
  • strongly typed result handling
  • discriminated unions for responses
  • extension methods for common operations

He also built helpers for:

  • optimistic concurrency
  • async enumeration
  • counting items efficiently
  • soft delete patterns

The result is a more idiomatic F# experience with lower cognitive load.

 

Programmability: JavaScript, Triggers, and Azure Functions

Cosmos DB supports JavaScript-based programmability inside the database:

  • stored procedures
  • user-defined functions
  • triggers

These run within a partition, which is an important limitation to remember.

Andrii showed an example of implementing an auto-increment counter using a trigger and a special counter document in the same container.

He also discussed a more scalable option: Azure Functions triggered by Cosmos DB changes.

This enables:

  • projection building
  • integration workflows
  • outbox/event-style patterns
  • change propagation outside the DB

 

Infrastructure and Deployment

For infrastructure, Cosmos DB can be defined in Bicep, including:

  • containers
  • partition keys
  • indexing policies
  • computed properties
  • unique constraints

However, one limitation is that Bicep does not support deploying:

  • triggers
  • stored procedures
  • user-defined functions

Those still need to be pushed via code.

Andrii also covered:

  • passwordless authentication via Entra ID
  • local development with the Cosmos emulator
  • CI usage
  • and a custom parser he wrote to bridge Bicep definitions into local emulator setup

 

Event Sourcing and Transactional Outbox Patterns

Toward the end, Andrii explored how Cosmos DB fits into event-driven architectures.

He described two patterns:

Event sourcing

Cosmos can be used for:

  • event storage
  • projections
  • projection rebuilding with Azure Functions

Transactional outbox

This was his preferred approach in practice.

Instead of storing events as the source of truth, he:

  1. writes the entity
  2. writes a draft event
  3. uses an Azure Function triggered by the entity write
  4. finalizes the event if the entity write succeeded

This avoids some of the consistency gaps that can appear when projections lag behind events.

It also captures manual changes made directly in the database by turning them into events automatically.

 

Final Takeaways

The talk’s main lessons were:

  • Cosmos DB is powerful, but design matters
  • Partitioning is everything
  • Storage is cheap, compute is expensive
  • Denormalization is usually the right choice
  • F# can make Cosmos significantly easier to use
  • Azure Functions and Cosmos triggers open the door to rich event-driven patterns

For teams already in Azure, Cosmos DB can be a strong fit when you need:

  • scale
  • global distribution
  • flexible document models
  • modern features like vector search
  • or low-friction throughput management

And with the right F# abstractions, it becomes much more pleasant to work with day to day.

 

Additional Resources

Check out more from the MeetUp Func Prog Sweden. Func Prog Sweden is the community for anyone interested in functional programming. At the MeetUps the community explore different functional languages like Erlang, Elixir, Haskell, Scala, Clojure, OCaml, F# and more.