The container

The line-up from the first chapter is hard-coded, and it never notices that last week's concerts are over. Let's give the staff a file they can edit, and hide the concerts that already happened. Both the lineup command and the concert page need that logic, and this is exactly what the container is for: it creates your objects once and hands them to whoever asks.

Singletons

A #[singleton] is created once and shared. A struct without fields needs nothing else. The box office keeps asking what time it is, so the clock becomes a singleton:

use chrono::DateTime;
use chrono::Utc;
use margaret::framework::macros::singleton;

#[singleton]
pub struct Clock;

impl Clock {
    pub fn now(&self) -> DateTime<Utc> {
        Utc::now()
    }
}

Asking a singleton for the time, instead of calling Utc::now() everywhere, keeps the time in one place. Every chapter from here on asks the Clock.

Constructors

A struct with fields gets a #[constructor]. The Lineup reads the staff's lineup.json when it's created. Its constructor is async, because reading a file takes a moment, and it returns a Result, because the file might be missing:

use std::sync::Arc;

use margaret::framework::macros::constructor;
use margaret::framework::macros::singleton;

use crate::clock::Clock;
use crate::show::Show;

#[singleton]
pub struct Lineup {
    clock: Arc<Clock>,
    shows: Vec<Show>,
}

impl Lineup {
    #[constructor]
    pub async fn load(clock: Arc<Clock>) -> anyhow::Result<Self> {
        let shows = serde_json::from_str(&tokio::fs::read_to_string("lineup.json").await?)?;

        Ok(Self { clock, shows })
    }

    pub fn upcoming(&self) -> Vec<&Show> {
        let now = self.clock.now();

        self.shows
            .iter()
            .filter(|show| show.starts_at > now)
            .collect()
    }
}

Each show is a plain struct, read with serde:

use chrono::DateTime;
use chrono::Utc;
use serde::Deserialize;

#[derive(Deserialize)]
pub struct Show {
    pub artist: String,
    pub starts_at: DateTime<Utc>,
}
[
  { "artist": "Paper Lanterns", "starts_at": "2026-11-14T20:00:00Z" },
  { "artist": "The Midnight Owls", "starts_at": "2026-11-21T19:30:00Z" },
  { "artist": "Velvet Static", "starts_at": "2026-12-05T20:00:00Z" }
]

Add chrono (with serde), serde (with derive), and serde_json to your dependencies, and the fs feature to tokio.

Injecting dependencies

Singletons ask for each other as Arc constructor parameters. The Lineup asked for the Clock, so Margaret builds the Clock first. The command asks for the Lineup, and only prints what's still ahead:

use std::sync::Arc;

use margaret::framework::console::command_outcome::CommandOutcome;
use margaret::framework::macros::console_command;
use margaret::framework::macros::constructor;
use margaret::framework::macros::process;
use margaret::framework::macros::singleton;

use crate::lineup::Lineup;

#[singleton]
#[console_command(name = "lineup", description = "Shows the upcoming concerts")]
pub struct ShowLineup {
    lineup: Arc<Lineup>,
}

impl ShowLineup {
    #[constructor]
    pub fn create(lineup: Arc<Lineup>) -> anyhow::Result<Self> {
        Ok(Self { lineup })
    }

    #[process]
    pub fn run(&self) -> anyhow::Result<CommandOutcome> {
        for show in self.lineup.upcoming() {
            println!("{}  {}", show.starts_at.format("%b %e"), show.artist);
        }

        Ok(CommandOutcome::Succeeded)
    }
}
$ box_office lineup
Nov 14  Paper Lanterns
Nov 21  The Midnight Owls
Dec  5  Velvet Static

The /concerts page asks for the same Lineup in its constructor, and gets the very same instance.

When construction fails

If a constructor returns an error, the command stops before doing anything:

$ box_office lineup
failed to construct 'crate::lineup::Lineup': No such file or directory (os error 2)

When singletons are built

  • A command builds only what it needs, so lineup builds the Clock and the Lineup, and nothing else.

  • serve builds every singleton that a server or a background service needs, once, before it starts.

What the build checks

Asking for something that isn't a singleton is a build error, and so are dependency cycles. A cycle names every step:

Error: Container { source: DependencyCycle { path: "crate::clock::Clock -> crate::lineup::Lineup -> crate::clock::Clock" } }

A singleton that nothing uses is an error too. Add the Clock before anything asks for it, and the build refuses:

Error: Container { source: UnconsumedSingleton { path: "crate::clock::Clock" } }

Margaret matches dependencies by their full path, so two structs with the same name in different modules are two different singletons.