Chapter 2.1: Setting Up Your Project

Let’s create a new Rust project and add EventCore dependencies. We’ll build a task management system that demonstrates EventCore’s key features.

Create a New Project

cargo new taskmaster --bin
cd taskmaster

Add Dependencies

Edit Cargo.toml to include EventCore and related dependencies:

[package]
name = "taskmaster"
version = "0.1.0"
edition = "2021"

[dependencies]
# EventCore core functionality
eventcore = "0.1"
eventcore-macros = "0.1"

# For development/testing - switch to eventcore-postgres for production
eventcore-memory = "0.1"

# Async runtime
tokio = { version = "1.40", features = ["full"] }
async-trait = "0.1"

# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Type validation
nutype = { version = "0.6", features = ["serde"] }

# Utilities
uuid = { version = "1.11", features = ["v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
thiserror = "2.0"

# For our CLI interface
clap = { version = "4.5", features = ["derive"] }

[dev-dependencies]
# Testing utilities
proptest = "1.6"

Project Structure

Create the following directory structure:

taskmaster/
├── Cargo.toml
├── src/
│   ├── main.rs           # Application entry point
│   ├── domain/
│   │   ├── mod.rs        # Domain module
│   │   ├── types.rs      # Domain types with validation
│   │   ├── events.rs     # Event definitions
│   │   └── commands/     # Command implementations
│   │       ├── mod.rs
│   │       ├── create_task.rs
│   │       ├── assign_task.rs
│   │       └── complete_task.rs
│   ├── projections/
│   │   ├── mod.rs        # Projections module
│   │   ├── task_list.rs  # User task lists
│   │   └── statistics.rs # Task statistics
│   └── api/
│       ├── mod.rs        # API module (we'll add this in Part 4)
│       └── handlers.rs   # HTTP handlers

Create the directories:

mkdir -p src/domain/commands
mkdir -p src/projections
mkdir -p src/api

Initial Setup Code

Let’s create the basic module structure:

src/main.rs

mod domain;
mod projections;

use clap::{Parser, Subcommand};
use eventcore::prelude::*;
use eventcore_memory::InMemoryEventStore;

#[derive(Parser)]
#[command(name = "taskmaster")]
#[command(about = "A task management system built with EventCore")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Create a new task
    Create {
        /// Task title
        title: String,
        /// Task description
        description: String,
    },
    /// List all tasks
    List,
    /// Run interactive demo
    Demo,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize event store (in-memory for now)
    let event_store = InMemoryEventStore::new();
    let executor = CommandExecutor::new(event_store);
    
    let cli = Cli::parse();
    
    match cli.command {
        Commands::Create { title, description } => {
            println!("Creating task: {} - {}", title, description);
            // We'll implement this in Chapter 2.3
        }
        Commands::List => {
            println!("Listing tasks...");
            // We'll implement this in Chapter 2.4
        }
        Commands::Demo => {
            println!("Running demo...");
            run_demo(executor).await?;
        }
    }
    
    Ok(())
}

async fn run_demo<ES: EventStore>(executor: CommandExecutor<ES>) 
-> Result<(), Box<dyn std::error::Error>> 
where
    ES::Event: From<domain::events::TaskEvent> + TryInto<domain::events::TaskEvent>,
{
    println!("🚀 EventCore Task Management Demo");
    println!("================================\n");
    
    // We'll add demo code as we build features
    
    Ok(())
}

src/domain/mod.rs

#![allow(unused)]
fn main() {
pub mod types;
pub mod events;
pub mod commands;

// Re-export commonly used items
pub use types::*;
pub use events::*;
}

src/domain/types.rs

#![allow(unused)]
fn main() {
use nutype::nutype;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// Validated task title - must be non-empty and reasonable length
#[nutype(
    sanitize(trim),
    validate(not_empty, len_char_max = 200),
    derive(
        Debug,
        Clone,
        PartialEq,
        Eq,
        AsRef,
        Serialize,
        Deserialize,
        Display
    )
)]
pub struct TaskTitle(String);

/// Validated task description
#[nutype(
    sanitize(trim),
    validate(len_char_max = 2000),
    derive(
        Debug,
        Clone,
        PartialEq,
        Eq,
        AsRef,
        Serialize,
        Deserialize
    )
)]
pub struct TaskDescription(String);

/// Validated comment text
#[nutype(
    sanitize(trim),
    validate(not_empty, len_char_max = 1000),
    derive(
        Debug,
        Clone,
        PartialEq,
        Eq,
        AsRef,
        Serialize,
        Deserialize
    )
)]
pub struct CommentText(String);

/// Validated user name
#[nutype(
    sanitize(trim),
    validate(not_empty, len_char_max = 100),
    derive(
        Debug,
        Clone,
        PartialEq,
        Eq,
        Hash,
        AsRef,
        Serialize,
        Deserialize,
        Display
    )
)]
pub struct UserName(String);

/// Strongly-typed task ID
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TaskId(Uuid);

impl TaskId {
    pub fn new() -> Self {
        Self(Uuid::now_v7())
    }
}

impl Default for TaskId {
    fn default() -> Self {
        Self::new()
    }
}

impl std::fmt::Display for TaskId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Task priority levels
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Priority {
    Low,
    Medium,
    High,
    Critical,
}

impl Default for Priority {
    fn default() -> Self {
        Self::Medium
    }
}

/// Task status - note we model this as events, not mutable state
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaskStatus {
    Open,
    InProgress,
    Completed,
    Cancelled,
}

impl Default for TaskStatus {
    fn default() -> Self {
        Self::Open
    }
}
}

src/domain/events.rs

#![allow(unused)]
fn main() {
use super::types::*;
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

/// Events that can occur in our task management system
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum TaskEvent {
    /// A new task was created
    Created {
        task_id: TaskId,
        title: TaskTitle,
        description: TaskDescription,
        creator: UserName,
        created_at: DateTime<Utc>,
    },
    
    /// Task was assigned to a user
    Assigned {
        task_id: TaskId,
        assignee: UserName,
        assigned_by: UserName,
        assigned_at: DateTime<Utc>,
    },
    
    /// Task was unassigned
    Unassigned {
        task_id: TaskId,
        unassigned_by: UserName,
        unassigned_at: DateTime<Utc>,
    },
    
    /// Task priority was changed
    PriorityChanged {
        task_id: TaskId,
        old_priority: Priority,
        new_priority: Priority,
        changed_by: UserName,
        changed_at: DateTime<Utc>,
    },
    
    /// Comment was added to task
    CommentAdded {
        task_id: TaskId,
        comment: CommentText,
        author: UserName,
        commented_at: DateTime<Utc>,
    },
    
    /// Task was completed
    Completed {
        task_id: TaskId,
        completed_by: UserName,
        completed_at: DateTime<Utc>,
    },
    
    /// Task was reopened after completion
    Reopened {
        task_id: TaskId,
        reopened_by: UserName,
        reopened_at: DateTime<Utc>,
        reason: Option<String>,
    },
    
    /// Task was cancelled
    Cancelled {
        task_id: TaskId,
        cancelled_by: UserName,
        cancelled_at: DateTime<Utc>,
        reason: Option<String>,
    },
}

// Required for EventCore's type conversion
impl TryFrom<&TaskEvent> for TaskEvent {
    type Error = std::convert::Infallible;
    
    fn try_from(value: &TaskEvent) -> Result<Self, Self::Error> {
        Ok(value.clone())
    }
}
}

src/domain/commands/mod.rs

#![allow(unused)]
fn main() {
mod create_task;
mod assign_task;
mod complete_task;

pub use create_task::*;
pub use assign_task::*;
pub use complete_task::*;
}

src/projections/mod.rs

#![allow(unused)]
fn main() {
mod task_list;
mod statistics;

pub use task_list::*;
pub use statistics::*;
}

Verify Setup

Let’s make sure everything compiles:

cargo build

You should see output like:

   Compiling taskmaster v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in X.XXs

Create a Simple Test

Add to src/main.rs:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::types::*;
    
    #[test]
    fn test_validated_types() {
        // Valid title
        let title = TaskTitle::try_new("Fix the bug").unwrap();
        assert_eq!(title.as_ref(), "Fix the bug");
        
        // Empty title should fail
        assert!(TaskTitle::try_new("").is_err());
        
        // Whitespace is trimmed
        let title = TaskTitle::try_new("  Trimmed  ").unwrap();
        assert_eq!(title.as_ref(), "Trimmed");
    }
    
    #[test]
    fn test_task_id_generation() {
        let id1 = TaskId::new();
        let id2 = TaskId::new();
        
        // IDs should be unique
        assert_ne!(id1, id2);
        
        // IDs should be sortable by creation time (UUIDv7 property)
        assert!(id1.0 < id2.0);
    }
}
}

Run the tests:

cargo test

Environment Setup for PostgreSQL (Optional)

If you want to use PostgreSQL instead of the in-memory store:

  1. Start PostgreSQL with Docker:
docker run -d \
  --name eventcore-postgres \
  -e POSTGRES_PASSWORD=password \
  -e POSTGRES_DB=taskmaster \
  -p 5432:5432 \
  postgres:17
  1. Update Cargo.toml:
[dependencies]
eventcore-postgres = "0.1"
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres"] }
  1. Set environment variable:
export DATABASE_URL="postgres://postgres:password@localhost/taskmaster"

Summary

We’ve set up:

  • ✅ A new Rust project with EventCore dependencies
  • ✅ Domain types with validation using nutype
  • ✅ Event definitions for our task system
  • ✅ Basic project structure
  • ✅ Test infrastructure

Next, we’ll model our domain using event modeling techniques →