Skip to content

Best Practices

Best practices for building performant, reliable applications with TerraScale.


Design partition keys that distribute data evenly and support your access patterns:

// Good: Distributes data by user
{ "pk": "user#123", "sk": "profile" }
{ "pk": "user#123", "sk": "order#001" }
// Avoid: All data in one partition
{ "pk": "all_users", "sk": "user#123" }

Group related items under the same partition key:

// User and their orders in same partition
{ "pk": "user#123", "sk": "profile" }
{ "pk": "user#123", "sk": "order#2024-001" }
{ "pk": "user#123", "sk": "order#2024-002" }
// Query all orders efficiently
var filter = new QueryFilter
{
PartitionKey = "user#123",
SortKeyCondition = SortKeyCondition.BeginsWith("order#")
};

Distribute writes across partition keys:

// Bad: All writes to one partition
{ "pk": "orders", "sk": "order#12345" }
// Good: Partition by date or user
{ "pk": "orders#2024-01-15", "sk": "order#12345" }
{ "pk": "user#456#orders", "sk": "order#12345" }

Queries are efficient; scans read everything:

// Good: Query by partition key
var result = await client.QueryAsync(new QueryFilter
{
PartitionKey = "user#123"
});
// Avoid: Scan the entire database
var result = await client.ScanAsync(new PaginationOptions());

Always specify reasonable limits:

var result = await client.QueryAsync(filter, new QueryOptions
{
Limit = 50 // Don't fetch more than needed
});

Return only needed attributes:

var options = new QueryOptions
{
ProjectionAttributes = new[] { "name", "email" } // Skip large attributes
};

Structure keys to support your queries:

// Access pattern: Get user's orders by date
{ "pk": "user#123", "sk": "order#2024-01-15#001" }
// Now you can query by date range
SortKeyCondition.Between("order#2024-01-01", "order#2024-01-31")

Never assume operations succeed:

var result = await client.GetItemAsync("user#123");
if (result.IsFailed)
{
logger.LogError("Get failed: {Error}", result.Errors.First().Message);
return null;
}
return result.Value;

Handle transient failures gracefully:

var client = new TerraScaleDatabase(new TerraScaleDatabaseOptions
{
Retry = new RetryPolicyOptions
{
MaxRetries = 3,
BaseDelay = TimeSpan.FromMilliseconds(500),
UseJitter = true
}
});

Include operation details for debugging:

if (result.IsFailed)
{
logger.LogError(
"Failed to get item PK={Pk} SK={Sk}: {Error}",
pk, sk, result.Errors.First().Message
);
}

Transactions have higher latency than batch operations:

// Use batch for independent writes
await client.BatchWriteAsync(items);
// Use transactions only for atomic operations
await client.TransactWriteAsync(items);

Fewer items = faster execution:

// Good: Small, focused transaction
var items = new List<TransactWriteItem>
{
new() { Action = TransactAction.Put, ... },
new() { Action = TransactAction.Update, ... }
};
// Avoid: Large transactions with many items

Prevent duplicate operations on retry:

var result = await client.TransactWriteAsync(
items,
clientRequestToken: "order-12345-payment"
);

Reduce network round trips:

// Bad: Many individual requests
foreach (var key in keys)
{
await client.GetItemAsync(key.Pk, key.Sk);
}
// Good: Single batch request
await client.BatchGetAsync(keys);

Process independent operations concurrently:

var tasks = partitions.Select(pk =>
client.QueryAsync(new QueryFilter { PartitionKey = pk })
);
var results = await Task.WhenAll(tasks);

Reuse client instances:

// Good: Create once, reuse
public class MyService
{
private readonly TerraScaleDatabase _client;
public MyService(TerraScaleDatabase client)
{
_client = client;
}
}
// Avoid: Creating new clients per request

Grant minimum necessary permissions:

// Good: Specific permissions
{ "scopes": ["database:read", "repository:read"] }
// Avoid: Overly broad permissions
{ "scopes": ["*"] }

Set expiration and rotate regularly:

await client.ApiKeys.CreateAsync(new CreateApiKeyRequest(
Name: "Production Key",
Scopes: new[] { "database:read", "database:write" },
ExpiresAt: DateTime.UtcNow.AddMonths(3)
));

Protect accounts with two-factor authentication.

Never commit API keys to source control:

// Good: Environment variable
var apiKey = Environment.GetEnvironmentVariable("TERRASCALE_API_KEY");
// Good: Secret manager
var apiKey = await secretManager.GetSecretAsync("terrascale-api-key");
// Bad: Hardcoded
var apiKey = "ts_live_abc123...";

Typed entities with schema validation:

public record Customer : EntityBase
{
public required string Name { get; init; }
public required string Email { get; init; }
}
var customers = client.GetRepository<Customer>("customers");

Dynamic attributes without schema:

var item = new DatabaseItem
{
PartitionKey = "config#app",
Attributes = configData
};

Store data in the shape you read it:

// Instead of joining user and address
{ "pk": "user#123", "sk": "profile", "data": {
"name": "John",
"address": { // Embedded, not referenced
"street": "123 Main St",
"city": "NYC"
}
}}

Monitor your usage against plan limits:

var usage = await client.Payment.GetUsageAsync();
if (usage.Value.TotalRequests > warningThreshold)
{
logger.LogWarning("Approaching request limit");
}

Monitor API availability:

Terminal window
curl https://api.terrascale.io/health