ProgrammingAnalysis

Writer's Block: Three Ways to Beat the Blank File

admin14 min read

In software development, reading is easy; writing is hard.

We spend hours navigating existing codebases. We can trace execution paths, understand dependencies, and debug complex issues. But when faced with a blank file and a complex problem, many of us freeze.

This is technical Writer's Block: you know the destination, but the first step is invisible.

If you've ever found yourself stuck at the starting line, unsure how to translate a diagram into a single line of code, this article is for you.

The Scenario: OTC Dealer‑to‑Dealer Order Matching

Let's ground this in a concrete example from financial trading. Imagine you need to build an order matching engine for an over‑the‑counter (OTC) market where multiple dealers trade directly with each other (dealer‑to‑dealer, or D2D).

D2D Order matching engine

D2D Order matching engine

The requirements:

  • Multiple dealers (1, 2, 3, … N) participate. Each dealer can send both buy and sell orders at any time.
  • An order always specifies a counterpart dealer (the dealer with whom they wish to trade). The dealer sending the order is the originator.
  • Each order is for a specific financial instrument, identified by its ISIN (International Securities Identification Number).
  • The MatchingEngine receives all orders. It matches a buy order from dealer A to dealer B with a sell order from dealer B to dealer A, provided they have the same ISIN, price, and quantity.
  • Whenever a match occurs, the engine notifies downstream systems: a TradeConfirmation service (to send confirmations to both dealers) and a PositionKeeper (to update each dealer's holdings).

You open Visual Studio. You create the new project. And then... nothing.

Where do you start? The matching logic? The communication with dealers? The notifications?

Here are three different ways to break the paralysis. They are not steps, they are alternative approaches. Use whichever fits your mood and the problem at hand.


Way 1: Test as the First Client

When you don't know where to start, stop trying to build the machine. Write a test that uses your system the way a real client would, even if you haven't built anything yet. That test becomes your first client and your compass.

In this approach, you abstract away everything except the component you're currently building. Use mocks or simple stubs for dependencies.

Let's say we decide to build the MatchingEngine first. We know it needs to receive orders from dealers and notify downstream systems when a match occurs. We'll write a test that uses mock dealer sources and collects events. To avoid unreliable Thread.Sleep calls, we'll use a ManualResetEventSlim to wait for the expected event.

[Test]
public void MatchingEngine_WhenMatchingOrdersArrive_CreatesTradeAndNotifies()
{
    // Arrange
    var mockDealer1 = new Mock<IDealerSource>();
    var mockDealer2 = new Mock<IDealerSource>();
    var engine = new MatchingEngine();
    
    Trade receivedTrade = null;
    var tradeReceivedEvent = new ManualResetEventSlim(false);
    engine.TradeExecuted += (sender, trade) =>
    {
        receivedTrade = trade;
        tradeReceivedEvent.Set();
    };
    
    engine.Subscribe(mockDealer1.Object);
    engine.Subscribe(mockDealer2.Object);
    
    // Act: Simulate a buy order from Dealer 1 (originator) to Dealer 2 (counterparty),
    // and a matching sell order from Dealer 2 (originator) to Dealer 1 (counterparty)
    var buyOrder = new Order(
        originator: "Dealer1",
        counterparty: "Dealer2",
        side: Side.Buy,
        isin: "US0378331005", // Apple Inc. ISIN
        price: 150.25m,
        quantity: 100
    );
    
    var sellOrder = new Order(
        originator: "Dealer2",
        counterparty: "Dealer1",
        side: Side.Sell,
        isin: "US0378331005",
        price: 150.25m,
        quantity: 100
    );
    mockDealer1.Raise(s => s.OrderReceived += null, buyOrder);
    mockDealer2.Raise(s => s.OrderReceived += null, sellOrder);
   
    // Assert: Wait for the event (with timeout) and verify the trade
    Assert.IsTrue(tradeReceivedEvent.Wait(1000), "Trade event was not  raised");
    Assert.IsNotNull(receivedTrade);
    Assert.AreEqual("Dealer1", receivedTrade.Buyer);
    Assert.AreEqual("Dealer2", receivedTrade.Seller);
    Assert.AreEqual("US0378331005", receivedTrade.Isin);
    Assert.AreEqual(150.25m, receivedTrade.Price);
    Assert.AreEqual(100, receivedTrade.Quantity);
}

This test is completely isolated. It doesn't need real dealer sources, trade confirmations, or position keepers. It defines the contract we expect from the engine.

From this test, we derive the interfaces and types it implies:

public enum Side { Buy, Sell }

public class Order
{
    public string Originator { get; }
    public string Counterparty { get; }
    public Side Side { get; }
    public string Isin { get; }
    public decimal Price { get; }
    public int Quantity { get; }
    
    public Order(string originator, string counterparty, Side side, string isin, decimal price, int quantity)
    {
        Originator = originator;
        Counterparty = counterparty;
        Side = side;
        Isin = isin;
        Price = price;
        Quantity = quantity;
    }
}

public record Trade(string Buyer, string Seller, string Isin, decimal Price, int Quantity, DateTime TradeDate);

public interface IDealerSource
{
    event EventHandler<Order> OrderReceived;
    void Start();
    void Stop();
}

public interface IMatchingEngine
{
    void Subscribe(IDealerSource source);
    event EventHandler<Trade> TradeExecuted;
}

Now we implement MatchingEngine to make the test pass. We need a way to group orders for matching. Let's create a key type:

public record OrderKey(string Originator, string Counterparty, string Isin, decimal Price);

public class MatchingEngine : IMatchingEngine
{
    private readonly ConcurrentDictionary<OrderKey, ConcurrentQueue<Order>> _buyOrders = new();
    private readonly ConcurrentDictionary<OrderKey, ConcurrentQueue<Order>> _sellOrders = new();
    
    public event EventHandler<Trade> TradeExecuted;

    public void Subscribe(IDealerSource source)
    {
        source.OrderReceived += OnOrderReceived;
    }

    private void OnOrderReceived(object sender, Order order)
    {
        var key = new OrderKey(order.Originator, order.Counterparty, order.Isin, order.Price);
        
        // Add order to appropriate queue
        var queue = order.Side == Side.Buy 
            ? _buyOrders.GetOrAdd(key, _ => new ConcurrentQueue<Order>())
            : _sellOrders.GetOrAdd(key, _ => new ConcurrentQueue<Order>());
        
        queue.Enqueue(order);
        
        // Try to match
        TryMatch(order);
    }

    private void TryMatch(Order order)
    {
        var matchingKey = new OrderKey(
            order.Counterparty, 
            order.Originator, 
            order.Isin, 
            order.Price
        );
        
        var oppositeDict = order.Side == Side.Buy ? _sellOrders : _buyOrders;
        
        if (oppositeDict.TryGetValue(matchingKey, out var oppositeQueue) && 
            oppositeQueue.TryDequeue(out var oppositeOrder))
        {
            if (order.Side == Side.Buy)
                ExecuteTrade(order, oppositeOrder);
            else
                ExecuteTrade(oppositeOrder, order);
        }
    }

    private void ExecuteTrade(Order buy, Order sell)
    {
        var trade = new Trade(
            Buyer: buy.Originator,
            Seller: sell.Originator,
            Isin: buy.Isin,
            Price: buy.Price,
            Quantity: Math.Min(buy.Quantity, sell.Quantity),
            TradeDate: DateTime.UtcNow
        );
        TradeExecuted?.Invoke(this, trade);
    }
}

Test passes. Now we move to the next component: a concrete dealer source that simulates incoming orders. We'll write a test for it, again using ManualResetEventSlim to wait for the first order.

[Test]
public void DealerSource_RaisesEventWhenStarted()
{
    var dealer = new SimulatedDealerSource("Dealer1", intervalMs: 100);
    Order? received = null;
    var orderReceivedEvent = new ManualResetEventSlim(false);
    
    dealer.OrderReceived += (s, o) =>
    {
        received = o;
        orderReceivedEvent.Set();
    };
    
    dealer.Start();
    
    // Wait for the first order (with timeout)
    Assert.IsTrue(orderReceivedEvent.Wait(500), "No order received within timeout");
    
    dealer.Stop();
    
    Assert.IsNotNull(received);
}

We implement SimulatedDealerSource with a timer that generates random orders:

public class SimulatedDealerSource : IDealerSource
{
    private readonly string _dealerId;
    private readonly int _intervalMs;
    private Timer _timer;
    private readonly Random _random = new();
    private readonly string[] _counterparties = { "Dealer2", "Dealer3", "Dealer4" };
    private readonly string[] _isins = { "US0378331005", "US5949181045", "DE000BASF111" };

    public event EventHandler<Order> OrderReceived;

    public SimulatedDealerSource(string dealerId, int intervalMs = 2000)
    {
        _dealerId = dealerId;
        _intervalMs = intervalMs;
    }

    public void Start()
    {
        _timer = new Timer(_ =>
        {
            var side = _random.Next(2) == 0 ? Side.Buy : Side.Sell;
            var counterparty = _counterparties[_random.Next(_counterparties.Length)];
            var isin = _isins[_random.Next(_isins.Length)];
            var price = 100 + (decimal)_random.NextDouble() * 50;
            var quantity = _random.Next(1, 10) * 100;
            var order = new Order(_dealerId, counterparty, side, isin, price, quantity);
            OrderReceived?.Invoke(this, order);
        }, null, 0, _intervalMs);
    }

    public void Stop() => _timer?.Dispose();
}

Test passes. Next we implement the downstream clients. They simply subscribe to the engine's TradeExecuted event:

public class TradeConfirmation
{
    public void Subscribe(IMatchingEngine engine)
    {
        engine.TradeExecuted += (s, trade) =>
        {
            Console.WriteLine($"Trade confirmed: {trade.Quantity} {trade.Isin} @ {trade.Price:C} between {trade.Buyer} and {trade.Seller}");
        };
    }
}

public class PositionKeeper
{
    public void Subscribe(IMatchingEngine engine)
    {
        engine.TradeExecuted += (s, trade) =>
        {
            Console.WriteLine($"Position updated: {trade.Buyer} buys {trade.Quantity} {trade.Isin}, {trade.Seller} sells");
        };
    }
}

We now have a working system, built one component at a time, each driven by a focused test.


Way 2: Create a Class for Everything, Then Introduce Interfaces

Sometimes you just need to get something down on paper. Look at your diagram. Identify every noun (every box, every actor) and create a class for it. Don't worry about interfaces or abstractions yet. Just create the skeleton.

public class DealerSource
{
    public void Start() { /* TODO */ }
    public void Stop() { /* TODO */ }
}

public class MatchingEngine
{
    public void Subscribe(DealerSource source) { /* TODO */ }
}

public class TradeConfirmation
{
    public void Subscribe(MatchingEngine engine) { /* TODO */ }
}

public class PositionKeeper
{
    public void Subscribe(MatchingEngine engine) { /* TODO */ }
}

Now your project has shape. The blank page is gone. You have files, methods, and TODOs.

Now start connecting them. When a class references another one, that's the moment to introduce an interface. This is the dependency inversion principle in action: depend on abstractions, not concretions.

For example, when MatchingEngine needs to reference DealerSource, don't reference the concrete class. Introduce IDealerSource first:

public interface IDealerSource
{
    void Start();
    void Stop();
}

public class DealerSource : IDealerSource 
{
    public void Start() { /* TODO */ }
    public void Stop() { /* TODO */ }
}

Now modify MatchingEngine to depend on the interface:

public class MatchingEngine
{
    public void Subscribe(IDealerSource source)
    {
        // Now how the source can notify the engine of new orders
    }
}

But how does the source notify the engine? When implementing Subscribe, you realize the source needs a way to communicate orders. You have several options:

- Events: The source exposes an event that the engine subscribes to.

- Callbacks: The engine passes a delegate to the source.

- Observer pattern: The source maintains a list of observers.

- Message queue: Sources publish to a queue that the engine consumes.

For simplicity, we'll use events. Add the event to the interface:

public interface IDealerSource
{
    event EventHandler<Order> OrderReceived;
    void Start();
    void Stop();
}

This forces us to define Order. Let's create it:

public enum Side { Buy, Sell }

public class Order
{
    public string Originator { get; }
    public string Counterparty { get; }
    public Side Side { get; }
    public string Isin { get; }
    public decimal Price { get; }
    public int Quantity { get; }
    
    public Order(string originator, string counterparty, Side side, string isin, decimal price, int quantity)
    {
        Originator = originator;
        Counterparty = counterparty;
        Side = side;
        Isin = isin;
        Price = price;
        Quantity = quantity;
    }
}

Similarly, when TradeConfirmation and PositionKeeper need to receive trades from the engine, add an event to IMatchingEngine:

public interface IMatchingEngine
{
    void Subscribe(IDealerSource source);
    event EventHandler<Trade> TradeExecuted;
}

public record Trade(string Buyer, string Seller, string Isin, decimal Price, int Quantity, DateTime TradeDate);

Now we need a way to group orders for matching. Create a key type:

public record OrderKey(string Originator, string Counterparty, string Isin, decimal Price);

Now implement the engine with concurrent collections for thread safety:

public class MatchingEngine : IMatchingEngine
{
    private readonly ConcurrentDictionary<OrderKey, ConcurrentQueue<Order>> _buyOrders = new();
    private readonly ConcurrentDictionary<OrderKey, ConcurrentQueue<Order>> _sellOrders = new();
    
    public event EventHandler<Trade> TradeExecuted;

    public void Subscribe(IDealerSource source)
    {
        source.OrderReceived += OnOrderReceived;
    }

    private void OnOrderReceived(object sender, Order order)
    {
        var key = new OrderKey(order.Originator, order.Counterparty, order.Isin, order.Price);
        
        var queue = order.Side == Side.Buy 
            ? _buyOrders.GetOrAdd(key, _ => new ConcurrentQueue<Order>())
            : _sellOrders.GetOrAdd(key, _ => new ConcurrentQueue<Order>());
        
        queue.Enqueue(order);
        TryMatch(order);
    }

    private void TryMatch(Order order)
    {
        var matchingKey = new OrderKey(
            order.Counterparty, 
            order.Originator, 
            order.Isin, 
            order.Price
        );
        
        var oppositeDict = order.Side == Side.Buy ? _sellOrders : _buyOrders;
        
        if (oppositeDict.TryGetValue(matchingKey, out var oppositeQueue) && 
            oppositeQueue.TryDequeue(out var oppositeOrder))
        {
            if (order.Side == Side.Buy)
                ExecuteTrade(order, oppositeOrder);
            else
                ExecuteTrade(oppositeOrder, order);
        }
    }

    private void ExecuteTrade(Order buy, Order sell)
    {
        var trade = new Trade(
            Buyer: buy.Originator,
            Seller: sell.Originator,
            Isin: buy.Isin,
            Price: buy.Price,
            Quantity: Math.Min(buy.Quantity, sell.Quantity),
            TradeDate: DateTime.UtcNow
        );
        TradeExecuted?.Invoke(this, trade);
    }
}

Finally, implement the dealer source with a timer:

public class SimulatedDealerSource : IDealerSource
{
    private readonly string _dealerId;
    private readonly int _intervalMs;
    private Timer _timer;
    private readonly Random _random = new();
    private readonly string[] _counterparties = { "Dealer2", "Dealer3", "Dealer4" };
    private readonly string[] _isins = { "US0378331005", "US5949181045", "DE000BASF111" };

    public event EventHandler<Order> OrderReceived;

    public SimulatedDealerSource(string dealerId, int intervalMs = 2000)
    {
        _dealerId = dealerId;
        _intervalMs = intervalMs;
    }

    public void Start()
    {
        _timer = new Timer(_ =>
        {
            var side = _random.Next(2) == 0 ? Side.Buy : Side.Sell;
            var counterparty = _counterparties[_random.Next(_counterparties.Length)];
            var isin = _isins[_random.Next(_isins.Length)];
            var price = 100 + (decimal)_random.NextDouble() * 50;
            var quantity = _random.Next(1, 10) * 100;
            var order = new Order(_dealerId, counterparty, side, isin, price, quantity);
            OrderReceived?.Invoke(this, order);
        }, null, 0, _intervalMs);
    }

    public void Stop() => _timer?.Dispose();
}

And the downstream clients:

public class TradeConfirmation
{
    public void Subscribe(IMatchingEngine engine)
    {
        engine.TradeExecuted += (s, trade) =>
        {
            Console.WriteLine($"Trade confirmed: {trade.Quantity} {trade.Isin} @ {trade.Price:C} between {trade.Buyer} and {trade.Seller}");
        };
    }
}

public class PositionKeeper
{
    public void Subscribe(IMatchingEngine engine)
    {
        engine.TradeExecuted += (s, trade) =>
        {
            Console.WriteLine($"Position updated: {trade.Buyer} buys {trade.Quantity} {trade.Isin}, {trade.Seller} sells");
        };
    }
}

The key insight: we started with concrete classes, and each time we needed to connect two components, we introduced an interface. The design emerged naturally from the connections, not from upfront abstraction.


Way 3: Full Test-Driven Development

If you're comfortable with TDD, it's the most disciplined way to banish writer's block. The cycle is simple:

  1. Red: Write a failing test for the next behavior you want.
  2. Green: Write the simplest code to make it pass.
  3. Refactor: Clean up while keeping tests green.

For the matching engine, you might start with a dealer source:

[Test]
public void DealerSource_RaisesEventWhenStarted()
{
    var dealer = new SimulatedDealerSource("Dealer1", intervalMs: 100);
    Order? received = null;
    var orderReceivedEvent = new ManualResetEventSlim(false);
    
    dealer.OrderReceived += (s, o) =>
    {
        received = o;
        orderReceivedEvent.Set();
    };
    
    dealer.Start();
    
    Assert.IsTrue(orderReceivedEvent.Wait(500), "No order received");
    
    dealer.Stop();
    Assert.IsNotNull(received);
}

Run the test: it fails (red). Write the minimal code to pass:

public enum Side { Buy, Sell }

public class Order
{
    public string Originator { get; }
    public string Counterparty { get; }
    public Side Side { get; }
    public string Isin { get; }
    public decimal Price { get; }
    public int Quantity { get; }
    
    public Order(string originator, string counterparty, Side side, string isin, decimal price, int quantity)
    {
        Originator = originator;
        Counterparty = counterparty;
        Side = side;
        Isin = isin;
        Price = price;
        Quantity = quantity;
    }
}

public class SimulatedDealerSource
{
    public event EventHandler<Order> OrderReceived;

    public SimulatedDealerSource(string dealerId, int intervalMs) { }

    public void Start()
    {
        OrderReceived?.Invoke(this, new Order("Dealer1", "Dealer2", Side.Buy, "US0378331005", 100m, 100));
    }

    public void Stop() { }
}

Test passes (green). Now refactor: implement the timer properly, add randomness, etc.

Next, write a test for the matching engine. You'll need to introduce interfaces as you go:

[Test]
public void MatchingEngine_WhenOnlyOneOrder_NoTradeIsCreated()
{
    var mockDealer = new Mock<IDealerSource>();
    var engine = new MatchingEngine();
    var tradeEvent = new ManualResetEventSlim(false);
    engine.TradeExecuted += (_, __) => tradeEvent.Set();
    engine.Subscribe(mockDealer.Object);
    var buyOrder = new Order("Dealer1", "Dealer2", Side.Buy, "US0378331005", 150.25m, 100);
    mockDealer.Raise(s => s.OrderReceived += null, mockDealer.Object, buyOrder);
    Assert.IsFalse(tradeEvent.Wait(200), "Trade event should not be raised");
}

This test forces you to define IDealerSource and implement MatchingEngine minimally. Continue this cycle, building up the system one test at a time.

The TDD approach ensures you never write code without a failing test, and you always have a safety net.


Choosing the Right Approach

Each of the three ways has strengths and weaknesses. Which one you pick depends on your mood, the problem, and how stuck you feel.

Comparison of the three ways

Comparison of the three ways

There's no wrong choice. Pick the one that feels right today. The goal is the same: get past the blank file and start building.


## Putting It All Together

No matter which approach you take, the result is the same: a clean, decoupled system where components communicate via events.

The complete, runnable code is on GitHub:

🔗 github.com/lans-untout/kata-matching-engine

Notice the clean separation:

  • Dealers don't know about the engine.
  • Engine doesn't know about trade confirmation or position keeper.
  • Downstream systems only know about the engine's event.
  • Everything communicates via events.

A Note on Real-World Systems

The example above keeps things simple to focus on beating writer's block. In a production OTC D2D matching engine, you'd likely use a message broker (like Kafka, RabbitMQ, ...) instead of in‑process events. The matching engine would publish trades to a dedicated topic, and downstream services (trade confirmation, position keeper) would subscribe to that topic. This adds durability, scalability, and true decoupling across process boundaries (while the core matching logic remains unchanged).


Stop staring. Pick a way and start.

Comments

Leave a comment

Comments are reviewed before they appear.