Project 2: Key-Value Store Client Library
Build a robust C client library for a key-value server with explicit ownership and a consistent error model.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Intermediate |
| Time Estimate | 1-2 weeks |
| Main Programming Language | C (Alternatives: Rust, Zig) |
| Alternative Programming Languages | Rust, Zig |
| Coolness Level | Level 6 - real client API boundary |
| Business Potential | Level 6 - reusable network client foundation |
| Prerequisites | Sockets, memory ownership, error handling |
| Key Topics | TCP I/O, ownership contracts, error models |
1. Learning Objectives
By completing this project, you will:
- Design a stable, misuse-resistant C API for a network client.
- Implement a robust TCP client with partial read/write handling.
- Define ownership rules for returned data and enforce them.
- Build a consistent error model that distinguishes “not found” vs “error.”
- Create tests that validate correct behavior under failure conditions.
2. All Theory Needed (Per-Concept Breakdown)
2.1 TCP Socket Lifecycle & Framing
Fundamentals
A TCP client is a state machine: create socket -> connect -> send request -> receive response -> close. Unlike file I/O, TCP is a stream with no message boundaries. This means you must design framing: a way to delimit where a response begins and ends. If you assume recv returns a full response, your client will break under load or on slower networks. You also must handle partial writes (send returns fewer bytes than requested) and partial reads (recv returns partial data). A correct client loops until the entire message is sent and the entire response is read. This is the core boundary between your library and the network.
Deep Dive into the Concept
TCP is a byte stream, not a packet system. The server might send your response in two separate network packets or coalesce multiple responses into one. Your client can’t rely on any single recv call to return the complete message. Thus, you need a protocol that defines message boundaries. A common approach is line-based framing (\n terminated commands and responses) or length-prefixed messages (send LEN:<n>\n followed by n bytes). The choice affects your parser and your API design. Line-based is easy but fragile for binary data; length-prefixed is more robust and handles arbitrary bytes.
Partial writes are just as important. send(fd, buf, len, 0) may return k < len. Your client must retry sending the remaining bytes, otherwise the server sees a truncated command. Likewise, recv may return less than you expect, or it may return 0, which indicates EOF (server closed the connection). You must treat EOF as a disconnection and propagate a meaningful error. Timeouts are another boundary. Without timeouts, your library can block forever. You should use setsockopt with SO_RCVTIMEO/SO_SNDTIMEO or use poll/select to enforce a deadline.
Framing and buffering interact with ownership. If you parse a response line by line, you’ll likely buffer data internally. That buffer may be reused across calls, which affects whether you can return borrowed pointers. If you want to return char* values to the caller, you must either allocate a new buffer per response (caller owns) or require the caller to copy data before the next call. The safest contract is: “values returned by kv_get are owned by the caller and must be freed with kv_free().” This avoids reentrancy problems and makes the API safe across calls.
Error handling in network I/O is subtle. EINTR means the system call was interrupted; you should retry. EAGAIN/EWOULDBLOCK means a non-blocking socket would block; you can retry or return a timeout error. ECONNRESET indicates the peer reset the connection. These errors must map into your library’s error model. A well-designed client never exposes raw errno values directly; it translates them into stable error codes and optionally provides kv_last_error() for debugging.
Finally, consider protocol consistency. If you design a simple protocol for your key-value server, define it precisely: allowed characters in keys, maximum key/value lengths, and response formats. A client library is not just “send strings”; it must enforce protocol invariants to prevent injection and to detect server bugs. For example, if the server says VALUE 5 but sends only 3 bytes, your client must treat that as a protocol error. The network boundary is adversarial by default; robust clients validate everything.
How This Fits in This Project
Your client’s wire protocol, buffer management, and retry logic are built on this concept. You’ll implement framing in Sec. 3.5, handle partial I/O in Sec. 5.10 Phase 2, and test protocol errors in Sec. 6.2. This concept also feeds into Project 5’s HTTP parsing. Also used in: Project 5.
Definitions & Key Terms
- TCP stream -> Continuous byte stream with no message boundaries.
- Framing -> Protocol rules for message boundaries.
- Partial write/read ->
send/recvtransfers fewer bytes than requested. - EOF ->
recvreturns 0; peer closed connection. - Timeout -> Upper bound on wait time for I/O.
Mental Model Diagram (ASCII)
Client buffer -> send() -> [TCP stream] -> recv() -> parse frame
^ |
| v
retry loop length/line parser
How It Works (Step-by-Step)
- Create socket and connect to server.
- Serialize request into buffer with a clear delimiter or length prefix.
- Send buffer with a loop until all bytes are written.
- Read response in chunks until framing rules indicate complete message.
- Parse response; validate format and lengths.
- Translate errors into library status codes.
Minimal Concrete Example
// Send all bytes
size_t off = 0;
while (off < len) {
ssize_t n = send(fd, buf + off, len - off, 0);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) return KV_ERR_IO;
off += (size_t)n;
}
Common Misconceptions
- “
recvreturns a whole message.” -> It returns any available bytes. - “Line-based protocols are always safe.” -> They break on embedded newlines or binary values.
- “Timeouts are optional.” -> They prevent permanent hangs.
Check-Your-Understanding Questions
- Why must you loop on
sendandrecv? - What is the difference between EOF and timeout?
- Why is framing essential for a TCP protocol?
Check-Your-Understanding Answers
- Because TCP is a stream and I/O can be partial.
- EOF means peer closed; timeout means no data within deadline.
- Without framing, you cannot detect message boundaries.
Real-World Applications
- Redis and Memcached clients.
- HTTP client libraries.
- Database wire protocols.
Where You’ll Apply It
- In this project: Sec. 3.5 (protocol), Sec. 5.10 Phase 2 (I/O loops), Sec. 6.2 (tests).
- Also used in: Project 5.
References
- “The Linux Programming Interface” - Ch. 56-59 (Sockets)
- “UNIX Network Programming” by W. Richard Stevens - Vol. 1
man send,man recv,man setsockopt
Key Insight
TCP gives you bytes, not messages; your protocol and loops create correctness.
Summary
Correct socket clients require framing, partial I/O handling, and timeouts. These are the core techniques that make network boundaries predictable.
Homework/Exercises to Practice the Concept
- Write a client that sends a long message and handles partial writes.
- Simulate server delays and verify timeouts trigger cleanly.
- Implement both line-based and length-prefixed responses.
Solutions to the Homework/Exercises
- Use a loop around
senduntil all bytes are written. - Set
SO_RCVTIMEOand return a timeout error. - Parse
\nfor line-based; parseLENheader for length-prefixed.
2.2 Ownership & API Contracts in C
Fundamentals
Ownership is about who allocates and who frees memory. In a client library, ownership decisions must be unambiguous: if kv_get() returns a pointer, does the caller own it, or is it borrowed from an internal buffer? Ambiguity leads to leaks and use-after-free. A good contract uses explicit naming (kv_get_owned, kv_free_string) and stable types (opaque kv_handle). Const correctness communicates whether the caller can modify data. Together, these rules form the boundary contract between your library and its users.
Deep Dive into the Concept
C has no garbage collector or automatic ownership tracking. That means your API must make ownership explicit. The most robust pattern for a client library is to return owned data and provide a library-specific free function. Why not free() directly? Because your library might allocate from a custom allocator or attach metadata. Even if you use malloc now, providing kv_free_string() future-proofs the API and makes ownership obvious in code reviews.
Opaque handles are essential. If you expose struct kv_handle in your public header, any change to that struct breaks ABI. Instead, declare typedef struct kv_handle kv_handle; in the header and define the struct in the .c file. Users can only treat it as an opaque pointer, which stabilizes your ABI and lets you evolve internals freely. This also encodes ownership: the handle is created by kv_connect and destroyed by kv_disconnect. All functions that take a handle can assume it is valid only between those calls.
Const correctness further clarifies contracts. For input parameters like keys and values, use const char* to signal that the library will not modify them. For returned data, use const char* only if it is borrowed; otherwise return char* and document that the caller owns it. You should also define what happens on error: for example, kv_get() could return NULL and set an error code. But NULL could also represent “key not found.” To avoid ambiguity, return a status code and write the value via an output parameter. This pattern makes ownership and errors explicit and testable.
Another boundary is lifetime of returned data. If your library returns a pointer to an internal buffer that is reused on the next call, callers will experience data corruption. The safe policy is: each successful kv_get allocates a new buffer; the caller frees it with kv_free_string. This also makes the API thread-safe because concurrent calls do not share buffers. The cost is allocation overhead, but this is acceptable for correctness-first design. If you later want a zero-copy API, you can add a new function that returns a slice with documented lifetime constraints.
Finally, design for misuse resistance. Use names like kv_get_copy or kv_free_string to remind callers of ownership. Accept NULL pointers only where you can handle them safely, otherwise return KV_ERR_INVALID_ARG. Validate arguments aggressively at the API boundary. Defensive programming is part of the contract.
How This Fits in This Project
This concept drives your public header (kv.h), your handle design (Sec. 3.5), and your error model in Sec. 3.2. You will also test ownership contracts in Sec. 6.2. Ownership discipline here mirrors Project 3’s JSON tree and Project 4’s logging buffers. Also used in: Project 3, Project 4.
Definitions & Key Terms
- Ownership -> Responsibility for freeing memory.
- Opaque handle -> Type declared but not defined in header.
- Borrowed pointer -> Memory owned elsewhere; must not be freed.
- Const correctness -> Use
constto signal immutability.
Mental Model Diagram (ASCII)
caller allocates key/value (borrowed)
|
v
kv_get(handle, key, &out_value)
|
v
library allocates new string -> caller owns -> kv_free_string()
How It Works (Step-by-Step)
kv_connectallocateskv_handleand returns it to caller.kv_setborrows key/value and sends request.kv_getallocates a new buffer for the value.- Caller frees with
kv_free_string. kv_disconnectfrees the handle and closes socket.
Minimal Concrete Example
kv_status kv_get(kv_handle *h, const char *key, char **out_value);
void kv_free_string(char *s);
Common Misconceptions
- “Returning
const char*means owned data.” -> It means borrowed. - “
free()is always fine.” -> API-specific free preserves ABI. - “Opaque types are overkill.” -> They protect ABI stability.
Check-Your-Understanding Questions
- Why should
kv_getallocate a new buffer? - What is the risk of exposing
struct kv_handlein the header? - How does const correctness prevent misuse?
Check-Your-Understanding Answers
- It avoids lifetime ambiguity and makes the API thread-safe.
- Any internal change breaks ABI and user code.
- It prevents accidental modification of borrowed inputs.
Real-World Applications
- Database client libraries (PostgreSQL, MySQL).
- HTTP libraries returning response bodies.
Where You’ll Apply It
- In this project: Sec. 3.2 (API), Sec. 5.10 Phase 1 (handle design).
- Also used in: Project 3, Project 4.
References
- “Effective C” - Ch. 6 (Memory and Ownership)
- “C Interfaces and Implementations” - Ch. 1
Key Insight
Clear ownership rules eliminate an entire class of API misuse bugs.
Summary
Ownership is a boundary contract. Opaque handles, explicit free functions, and const-correct signatures make your client library safe and stable.
Homework/Exercises to Practice the Concept
- Sketch an API that returns borrowed data and list potential bugs.
- Refactor it into an owned-data API.
- Write unit tests that assert ownership by freeing returned values.
Solutions to the Homework/Exercises
- Borrowed buffers can be invalidated on the next call.
- Allocate per call and return via output parameter.
- Verify valgrind reports no leaks.
2.3 Error Models & Resilient Client Behavior
Fundamentals
An error model defines how failures are reported, how detailed errors are retrieved, and what state remains after failure. For a client library, you must distinguish between “key not found,” “network error,” “protocol error,” and “invalid arguments.” Using a single error code or returning NULL for all failures is ambiguous. A good model uses a status enum plus a per-handle error string. It also guarantees that on failure, output parameters are set to safe values. This predictability makes the boundary reliable.
Deep Dive into the Concept
In C, the common error mechanisms are return codes, errno, and out-parameters. For a library, a return code enum is the clearest because it is stable and under your control. Define a kv_status enum with values like KV_OK, KV_ERR_CONN, KV_ERR_TIMEOUT, KV_ERR_PROTO, KV_ERR_NOT_FOUND, KV_ERR_INVALID_ARG. This makes failure modes explicit. Then define const char* kv_last_error(kv_handle*) to provide human-readable context. This string can be stored inside the handle, so it is thread-safe per handle.
Error models also define state transitions. If a network error occurs mid-request, is the connection still usable? A safe default is: if the protocol framing is compromised or the server closes the connection, mark the handle as “dirty” and require reconnect. This prevents subtle bugs where the client reads leftover bytes from a failed request. You can implement this by tracking a connected flag in the handle and returning KV_ERR_CONN on subsequent calls until the user reconnects.
You must also handle invalid input. If key is NULL or too long, return KV_ERR_INVALID_ARG immediately and do not touch network. This isolates boundary failures and improves debuggability. For “not found,” do not return NULL without status. Instead, return KV_ERR_NOT_FOUND and set *out_value = NULL. This avoids ambiguity and makes it easy to handle.
Mapping system errors to your enum requires care. For example, ECONNREFUSED maps to KV_ERR_CONN, ETIMEDOUT maps to KV_ERR_TIMEOUT, EPIPE (broken pipe) maps to KV_ERR_CONN. EINVAL might mean you used an invalid socket option, which is a programmer error; you can map it to KV_ERR_INTERNAL and include details in kv_last_error.
Finally, consider future extensibility. If you add new error codes later, old callers should still work. That means you should define a default case and avoid assuming the enum is exhaustive. Provide a stable numeric range and document it. This is how you preserve ABI and API contracts across versions.
How This Fits in This Project
Your error enum and kv_last_error function are central to Sec. 3.2 and Sec. 3.7. You’ll test error handling in Sec. 6.2 and debug error propagation in Sec. 7.1. The same approach is reused in Project 3’s parser errors and Project 5’s HTTP error responses. Also used in: Project 3, Project 5.
Definitions & Key Terms
- Error model -> Rules for detecting and reporting failures.
- Status code -> Enum representing specific failure types.
- Error string -> Human-readable detail, often per-handle.
- Fail-closed -> On error, invalidate state to prevent reuse.
Mental Model Diagram (ASCII)
kv_get() -> status code
-> if error: last_error(handle)
-> output pointer = NULL
How It Works (Step-by-Step)
- Validate inputs; return
KV_ERR_INVALID_ARGon failure. - Perform I/O; map system errors to
kv_status. - On success, set
*out_valueto newly allocated data. - On failure, set
*out_value = NULLand store error string. - If protocol corruption occurs, mark handle disconnected.
Minimal Concrete Example
typedef enum {
KV_OK = 0,
KV_ERR_NOT_FOUND = 1,
KV_ERR_CONN = -1,
KV_ERR_TIMEOUT = -2,
KV_ERR_PROTO = -3,
KV_ERR_INVALID_ARG = -4
} kv_status;
Common Misconceptions
- “NULL return is enough.” -> It hides “not found” vs “error.”
- “
errnois a good API.” -> It is global and easy to misuse. - “Errors don’t affect state.” -> Protocol errors can corrupt the stream.
Check-Your-Understanding Questions
- Why distinguish
NOT_FOUNDfromCONNerrors? - When should a handle be marked unusable after error?
- Why store error strings per handle?
Check-Your-Understanding Answers
- They require different caller actions (retry vs report missing key).
- When the protocol stream is corrupted or the peer disconnects.
- It’s thread-safe and avoids global state.
Real-World Applications
- Redis client libraries returning
REDIS_ERRvsREDIS_NIL. - HTTP libraries returning status codes plus error messages.
Where You’ll Apply It
- In this project: Sec. 3.2 (API), Sec. 3.7 (outcome), Sec. 6.2 (tests).
- Also used in: Project 3, Project 5.
References
- “Fluent C” - Ch. 5 (Error handling)
- “The Linux Programming Interface” - Ch. 6 (Errors)
Key Insight
A clear error model is a usability feature and a safety guarantee.
Summary
Return codes plus per-handle error strings create a predictable boundary. They allow callers to respond correctly without guesswork.
Homework/Exercises to Practice the Concept
- Design an enum that distinguishes four error types and
NOT_FOUND. - Write a test that forces a timeout and verifies error code.
- Simulate a protocol error and ensure the handle becomes unusable.
Solutions to the Homework/Exercises
- Use
KV_ERR_*values and a separateKV_ERR_NOT_FOUND. - Set short receive timeout and connect to a silent server.
- Inject a malformed response and mark handle disconnected.
3. Project Specification
3.1 What You Will Build
A C client library libkv that communicates with a simple key-value server using a line-based or length-prefixed protocol. The library exposes a stable API with explicit ownership rules and clear error handling, plus a demo CLI tool.
3.2 Functional Requirements
- Connect/Disconnect: Create and destroy a connection handle.
- SET/GET/DEL: Support basic operations.
- Timeouts: Configurable socket timeouts.
- Error Model: Return
kv_statusandkv_last_error. - Ownership: Returned values are owned by caller and freed with
kv_free_string.
3.3 Non-Functional Requirements
- Performance: Handle 1,000 ops/sec on localhost.
- Reliability: No leaks under
valgrind. - Usability: Clear and stable API with documentation.
3.4 Example Usage / Output
Connected to 127.0.0.1:6379
SET user:1 -> OK
GET user:1 -> "Alice"
GET missing -> (not found)
3.5 Data Formats / Schemas / Protocols
Request format (line-based):
SET <key> <len>\n<value bytes>\n
GET <key>\n
DEL <key>\n
Response format:
OK\n
NOT_FOUND\n
VALUE <len>\n<value bytes>\n
ERR <code> <msg>\n
3.6 Edge Cases
- Key contains whitespace or newline.
- Server closes connection mid-response.
- Response length mismatch.
- Timeout during read or write.
3.7 Real World Outcome
A user can call kv_set and kv_get from C code and receive reliable status codes and owned data.
3.7.1 How to Run (Copy/Paste)
make
./demo --host 127.0.0.1 --port 6379
3.7.2 Golden Path Demo (Deterministic)
Use a test server that returns fixed values for user:1 and user:2.
3.7.3 If CLI: Exact Terminal Transcript
$ ./demo --host 127.0.0.1 --port 6379
Connected to 127.0.0.1:6379
SET user:1 -> OK
GET user:1 -> "Alice"
GET missing -> (not found)
$ echo $?
0
Failure demo (connection refused):
$ ./demo --host 127.0.0.1 --port 9999
Error [KV_ERR_CONN]: Connection refused
$ echo $?
2
Exit Codes:
0success2connection error3protocol error4invalid arguments
4. Solution Architecture
4.1 High-Level Design
+--------------+ TCP +--------------+
| libkv client |<--------->| kv server |
| handle + API | | simple text |
+--------------+ +--------------+
4.2 Key Components
| Component | Responsibility | Key Decisions | |———–|—————-|—————| | Connection handle | Socket + error state | Opaque struct, per-handle errors | | Protocol encoder | Serialize commands | Length-prefixed values | | Protocol decoder | Parse responses | Strict length validation |
4.3 Data Structures (No Full Code)
typedef struct {
int fd;
int connected;
char last_error[256];
} kv_handle;
4.4 Algorithm Overview
Key Algorithm: kv_get
- Validate inputs.
- Send
GETrequest with send-all loop. - Read response header line.
- If
VALUE, read length and value bytes. - Allocate buffer and return owned string.
Complexity Analysis:
- Time: O(n) for value length
n. - Space: O(n) for returned buffer.
5. Implementation Guide
5.1 Development Environment Setup
cc --version
make --version
5.2 Project Structure
libkv/
|-- include/
| `-- kv.h
|-- src/
| |-- kv.c
| |-- net.c
| `-- protocol.c
|-- demo/
| `-- demo.c
`-- Makefile
5.3 The Core Question You’re Answering
“When a function returns a pointer over a network boundary, how do you make ownership unambiguous?”
5.4 Concepts You Must Understand First
- TCP streams and framing.
- Ownership and opaque handles.
- Consistent error models.
5.5 Questions to Guide Your Design
- How will you distinguish “not found” from “error”?
- Who owns returned strings and how are they freed?
- What happens after a protocol error?
5.6 Thinking Exercise
Simulate a response with an incorrect length. What should the client do? Should it disconnect immediately?
5.7 The Interview Questions They’ll Ask
- How do you handle partial reads in TCP?
- Why use an opaque handle?
- How do you prevent ownership confusion?
5.8 Hints in Layers
Hint 1: Use output parameters
kv_status kv_get(kv_handle *h, const char *key, char **out);
Hint 2: Provide a library free
void kv_free_string(char *s);
5.9 Books That Will Help
| Topic | Book | Chapter | |——-|——|———| | Sockets | “The Linux Programming Interface” | Ch. 56-59 | | Ownership | “Effective C” | Ch. 6 | | Error handling | “Fluent C” | Ch. 5 |
5.10 Implementation Phases
Phase 1: API and Handle (2-3 days)
Goals: define public API and error model.
Tasks: write kv.h, define enums, implement kv_connect and kv_disconnect.
Checkpoint: connect/disconnect without leaks.
Phase 2: Protocol and I/O (3-5 days)
Goals: implement send/recv loops and response parsing.
Tasks: add kv_set, kv_get, kv_del and strict parsing.
Checkpoint: demo works with a local server.
Phase 3: Hardening (2-3 days)
Goals: edge cases, timeouts, and testing. Tasks: add timeout config, simulate errors, add tests. Checkpoint: all tests pass; valgrind clean.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | Response framing | line-based vs length-prefixed | length-prefixed | Handles arbitrary data safely | | Ownership model | borrowed vs owned | owned | Avoids lifetime ambiguity | | Error reporting | errno vs status enum | status enum | Clear, stable API |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|———-|———|———-|
| Unit Tests | Protocol parsing | parse VALUE lines |
| Integration Tests | Server interaction | set/get/del |
| Failure Tests | Timeouts, disconnects | forced close |
6.2 Critical Test Cases
- Partial write: simulate short write; ensure full command sent.
- Malformed response: length mismatch ->
KV_ERR_PROTO. - Timeout: server delays ->
KV_ERR_TIMEOUT.
6.3 Test Data
GET user:1 -> VALUE 5\nAlice\n
GET missing -> NOT_FOUND\n
ERR 100 invalid\n
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|——–|———|———-|
| Reusing internal buffers | Corrupted results | Allocate per call |
| No framing | Stuck reads | Define strict protocol |
| Mixing errno and status codes | Confusing errors | Centralize error mapping |
7.2 Debugging Strategies
- Use
tcpdumporstraceto see wire data. - Add a debug log of raw responses.
7.3 Performance Traps
- Reallocating buffers on every chunk; use a growable buffer strategy.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add
PINGcommand. - Add a
kv_last_status()function.
8.2 Intermediate Extensions
- Add automatic reconnect.
- Add configurable retry policy.
8.3 Advanced Extensions
- Add async API with callbacks.
- Add TLS support via OpenSSL.
9. Real-World Connections
9.1 Industry Applications
- Cache clients for Redis/Memcached.
- Service discovery clients.
9.2 Related Open Source Projects
- hiredis - Redis C client with status codes.
- libmemcached - Memcached client library.
9.3 Interview Relevance
- Network I/O handling under load.
- Ownership and error model design.
10. Resources
10.1 Essential Reading
- “The Linux Programming Interface” - Ch. 56-59
- “Effective C” - Ch. 6
10.2 Video Resources
- “TCP Explained” - networking lecture series (searchable title)
10.3 Tools & Documentation
man socket,man connect,man send,man recv
10.4 Related Projects in This Series
- Project 3: JSON Parser Library - ownership in tree structures.
- Project 5: libhttp-lite - HTTP client/server boundary.
11. Self-Assessment Checklist
11.1 Understanding
- I can explain how TCP framing works.
- I can explain ownership in
kv_get. - I can distinguish
NOT_FOUNDfromERR.
11.2 Implementation
- All API functions meet requirements.
- Errors are consistent and documented.
- No leaks in demo under valgrind.
11.3 Growth
- I can explain protocol design in an interview.
- I documented pitfalls and fixes.
12. Submission / Completion Criteria
Minimum Viable Completion:
kv_connect,kv_set,kv_get,kv_disconnectimplemented.- Clear ownership rules and free function.
- Error model documented and tested.
Full Completion:
- Timeouts and protocol validation.
- Demo CLI with clear success/failure outputs.
- Tests covering error paths.
Excellence (Going Above & Beyond):
- Async client API.
- TLS or authentication support.