# spdlog base_sink vs sink: Understanding the Core Difference in spdlog's Sink Hierarchy

> Understand the spdlog base_sink vs sink difference. Discover how base_sink offers helper functions while sink defines the core interface for robust logging.

- Repository: [Gabi Melman/spdlog](https://github.com/gabime/spdlog)
- Tags: deep-dive
- Published: 2026-07-22

---

**The `spdlog::sinks::sink` class defines the pure virtual interface that every sink must implement, while `spdlog::sinks::base_sink` is a CRTP-based helper template that provides default implementations for common functionality, requiring only `sink_it_()` and `flush_()` to be defined by subclasses.**

The **gabime/spdlog** library separates sink interfaces from implementation details through two distinct abstractions in the `spdlog::sinks` namespace. Understanding the **difference between spdlog's `base_sink` and `sink` classes** is critical when extending the library with custom logging destinations, as the choice determines how much boilerplate you must write for level filtering, formatting, and thread synchronization.

## spdlog::sinks::sink: The Minimal Pure Virtual Interface

Located in [`include/spdlog/sinks/sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h), the **sink** class serves as the abstract contract that every concrete sink must satisfy. It declares four pure virtual methods that establish the fundamental operations: `log(const details::log_msg&)`, `flush()`, `set_pattern(const std::string&)`, and `set_formatter(std::unique_ptr<formatter>)`.

When inheriting directly from **sink**, you must implement all four methods yourself. This abstraction provides no built-in level filtering, formatter storage, or synchronization primitives, giving you complete control over every aspect of message handling. Use this approach only when you need to bypass the standard behaviors provided by the helper base class or when implementing sinks with unique threading models that conflict with the CRTP pattern used by **base_sink**.

## spdlog::sinks::base_sink: The CRTP Implementation Scaffold

Defined in [`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h), **base_sink** is a template class using the Curiously Recurring Template Pattern (CRTP) with the signature `template<typename Mutex> class base_sink`. Unlike the raw interface, **base_sink** provides concrete implementations for `log()`, `flush()`, `set_pattern()`, and `set_formatter()` while storing a `level_` member and a `formatter_` pointer internally.

The **base_sink** template handles routine tasks such as checking `should_log(level)` before processing messages and acquiring locks via the `Mutex` template parameter. It leaves only two protected pure virtual methods for subclasses to implement: **`sink_it_(const details::log_msg&)`** for actual message output and **`flush_()`** for buffer flushing. Nearly all built-in sinks—including `stdout_color_sink`, `basic_file_sink`, and `rotating_file_sink`—inherit from **base_sink** rather than **sink** directly because it eliminates repetitive boilerplate.

## Key Technical Differences

The distinction between these abstractions centers on implementation burden versus flexibility:

- **Interface Completeness**: **sink** requires four pure virtual implementations; **base_sink** requires only `sink_it_()` and `flush_()`.
- **Level Filtering**: **sink** provides no filtering logic, while **base_sink** automatically discards messages below the configured level via its `log()` implementation.
- **Formatter Management**: **base_sink** maintains a `formatter_` pointer and implements `set_pattern()`/`set_formatter()` automatically; **sink** forces manual formatter storage and formatting logic.
- **Thread Safety**: **base_sink** uses CRTP with a `Mutex` template parameter to enable optional locking; **sink** leaves thread synchronization entirely to the implementer.

Typically, you should treat **sink** as the low-level contract and **base_sink** as the standard scaffold for concrete implementations.

## Code Implementation Examples

### Custom Sink Using the Interface (Inheriting from sink)

When you need full control over formatting, filtering, and synchronization, inherit directly from **sink** and implement all virtual methods.

```cpp
#include <spdlog/sinks/sink.h>

class my_raw_sink : public spdlog::sinks::sink {
public:
    void log(const spdlog::details::log_msg& msg) override {
        // Manual formatting required
        std::string formatted = fmt::to_string(msg.payload);
        // Output logic here...
    }
    
    void flush() override {
        // Manual flush logic...
    }
    
    void set_pattern(const std::string& pattern) override {
        // Pattern handling...
    }
    
    void set_formatter(std::unique_ptr<spdlog::formatter> formatter) override {
        // Formatter storage management...
    }
};

```

### Custom Sink Using the Helper (Inheriting from base_sink)

For standard use cases, inherit from **base_sink** to leverage built-in filtering and thread safety.

```cpp
#include <spdlog/sinks/base_sink.h>

template<typename Mutex>
class my_custom_sink : public spdlog::sinks::base_sink<Mutex> {
protected:
    void sink_it_(const spdlog::details::log_msg& msg) override {
        // Formatter already initialized and stored by base
        fmt::memory_buffer formatted;
        this->formatter_->format(msg, formatted);
        
        // Actual output logic
        std::cout << fmt::to_string(formatted);
    }
    
    void flush_() override {
        std::cout << std::flush;
    }
};

```

Notice that `log()`, `flush()`, level checking, and locking are handled by **base_sink**; your implementation only defines the final output operations.

## Summary

- **`spdlog::sinks::sink`** ([`include/spdlog/sinks/sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/sink.h)) defines the pure virtual interface requiring manual implementation of `log()`, `flush()`, `set_pattern()`, and `set_formatter()`.
- **`spdlog::sinks::base_sink`** ([`include/spdlog/sinks/base_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/base_sink.h)) is a CRTP template implementing the **sink** interface, providing automatic level filtering, formatter management, and mutex locking while only requiring `sink_it_()` and `flush_()`.
- Choose **sink** when you need complete control over threading, formatting, or filtering; choose **base_sink** for standard sink behavior with minimal boilerplate.
- Built-in sinks like `stdout_color_sink` and `rotating_file_sink` demonstrate **base_sink** as the preferred foundation for spdlog extensions.

## Frequently Asked Questions

### Should I inherit from spdlog::sinks::sink or spdlog::sinks::base_sink when creating a custom sink?

Inherit from **base_sink** unless you have specific requirements that conflict with its default behavior. The **base_sink** class handles level filtering, formatter storage, and thread synchronization via its template parameter, reducing your implementation to just the output logic in `sink_it_()`. Only inherit directly from **sink** if you need to bypass the default level checking or implement custom formatter management that conflicts with **base_sink**'s internal storage.

### Does spdlog::sinks::base_sink provide thread safety?

Yes, **base_sink** uses a CRTP design with a template `Mutex` parameter (typically `std::mutex` or `spdlog::details::null_mutex`). When you instantiate `base_sink<std::mutex>`, the `log()` and `flush()` methods automatically lock the mutex before calling your `sink_it_()` or `flush_()` implementations. If you use `null_mutex`, the locking overhead is eliminated for single-threaded scenarios.

### What happens if I don't implement sink_it_() in my base_sink subclass?

Your code will fail to compile because **base_sink** declares `sink_it_(const details::log_msg&)` as a protected pure virtual method. This is the customization point where you define the actual output logic, and **base_sink** calls this method from its concrete `log()` implementation after performing level checks and acquiring locks.

### Where can I find real-world examples of sinks built on these base classes?

Examine the built-in sinks in the **gabime/spdlog** repository. Files like [`include/spdlog/sinks/stdout_color_sinks.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/stdout_color_sinks.h), [`include/spdlog/sinks/basic_file_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/basic_file_sink.h), and [`include/spdlog/sinks/rotating_file_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/rotating_file_sink.h) all inherit from **base_sink**. For an example of a minimal custom sink, review [`include/spdlog/sinks/stdout_sink.h`](https://github.com/gabime/spdlog/blob/main/include/spdlog/sinks/stdout_sink.h), which demonstrates the `sink_it_()` and `flush_()` pattern used throughout the library.