This project demonstrates how to scale write-heavy applications using database sharding with .NET and MySQL. Instead of trying to make one database faster, we distribute writes across multiple databases.
Scaling writes means handling more write operations (create, update, delete) as your application grows. When your application becomes popular, you'll have more users creating more data, and a single database can't keep up.
Imagine a social media app:
- 100 users โ Your database handles it fine
- 10,000 users โ Still manageable
- 1,000,000 users โ Database becomes a bottleneck
- 10,000,000 users โ Database crashes under the load
The Problem: Single databases have physical limits:
- CPU and memory constraints
- Connection limits
- Disk I/O bottlenecks
- Lock contention from concurrent writes
Database sharding solves this by splitting your data across multiple databases (called "shards"). Instead of one database handling all writes, you have multiple databases each handling a portion of your data.
flowchart TD
%% Traditional Approach
subgraph Traditional ["Traditional Approach"]
A1[App] --> B1[Single Database<br/>โ ๏ธ Bottleneck]
end
%% Sharded Approach
subgraph Sharded ["Sharded Approach"]
A2[App] --> C[Shard Router]
C --> D1[Database 1<br/>user data A-M]
C --> D2[Database 2<br/>user data N-Z]
C --> D3[Database 3<br/>user data 0-9]
end
%% Styling
classDef bottleneck fill:#ffebee,stroke:#f44336,stroke-width:2px
classDef shard fill:#e8f5e8,stroke:#4caf50,stroke-width:2px
classDef router fill:#e3f2fd,stroke:#2196f3,stroke-width:2px
class B1 bottleneck
class D1,D2,D3 shard
class C router
- Horizontal Scaling: Add more databases as you grow
- Better Performance: Write operations distribute across servers
- Fault Isolation: One shard failing doesn't crash everything
- Geographic Distribution: Put shards closer to users
This implementation uses consistent hashing to automatically determine which database should store each piece of data.
- Shard Resolution: When you create a user, the system automatically decides which database to use
- Data Distribution: Similar data goes to similar locations (users A-M together, N-Z together)
- Cross-Shard Queries: When you need data from all shards, it queries them in parallel
- Transparent Operation: Your application code doesn't need to know which database contains what
1. Create User "John Doe"
โ
2. System hashes "John Doe" โ "shard_2"
โ
3. User saved to database shard_2
โ
4. Return user ID to application
- .NET 10.0 SDK
- Docker and Docker Compose
# Start the database shards
docker-compose up -d
# Run the application
cd ScalingWrites.Core
dotnet run# Create a user (automatically routed to a shard)
curl -X POST "https://localhost:5001/api/users" \
-H "Content-Type: application/json" \
-d '{"name": "John Doe"}'
# The response includes a user ID
# Check which shard stored this user by looking at the database logsThe system creates 6 MySQL database shards:
- Shard 0: Port 3306
- Shard 1: Port 3307
- Shard 2: Port 3308
- Shard 3: Port 3309
- Shard 4: Port 3310
- Shard 5: Port 3311
When you create data:
- The system looks at the data's ID (GUID or other key)
- Uses consistent hashing to determine the shard
- Saves the data to the correct shard
- Returns the result to your application
What it does: Distributes data evenly across shards while minimizing reorganization when shards are added/removed.
Why it matters:
- New users are distributed fairly
- Adding new shards doesn't require moving most data
- Performance stays consistent as you scale
What they are: The piece of data used to decide which shard stores your information.
In this project:
- Users: Sharded by
User.Id(GUID) - Publications: Sharded by
Publication.Id(GUID) or associatedUser.Id
When needed: When you need data from multiple shards (like searching all users).
How it works:
- Query all shards in parallel
- Combine results into a single response
- Example: "Find all publications" queries all 6 shards
What it does: Different types of data use different sharding strategies.
In this project:
- GUID-based data: Uses hash-based distribution
- Numeric data: Could use range-based distribution
- Date-based data: Could group by date ranges
- High write volume (thousands of writes per second)
- Large datasets (millions of records)
- Growing applications with predictable growth
- Applications where data can be partitioned naturally
- Small applications (< 100K records)
- Applications requiring complex joins across all data
- When you can optimize a single database first
- Teams without DevOps/database expertise
- Monitoring: Track shard performance and data distribution
- Backup Strategy: Backup each shard separately
- Load Balancing: Route reads to replicas if needed
- Health Checks: Detect when shards go down
- Monitor shard utilization (don't let one get overloaded)
- Plan shard additions before you need them
- Test recovery procedures
- Consider read replicas for query performance
- Understand the concept of sharding
- See how this project distributes data
- Run the demo and observe shard usage
- Add a new entity type with sharding
- Implement custom sharding strategies
- Add monitoring and health checks
- Implement automatic shard rebalancing
- Add cross-shard transactions
- Set up geographic shard distribution
This project demonstrates production-ready sharding patterns. The code is structured for real-world use while remaining educational. Use it as a starting point for your own scalable applications!