The Wedding Cake Architecture

First, let me start by stating that I’m not claiming to have invented this. Not even close. I ‘borrowed’ the idea, from “Uncle Bob”. Interestingly enough, I stumbled upon it (kinda), before I ever read about it. Here’s the story.

I had had my doubts about frameworks and ORMs like Entity Framework for some time. What struck me as odd was a lot of the examples show direct usage of the table classes (classes that represent directly the data in the tables) at the API or UI level. Now, I understand the need for this in a blog post or an example for simplicity. What ends up happening though, is this becomes the foundation for reality.  Then once you’ve exposed classes that directly represent your table structure to an API, be it REST or WCF, you’ve unintentionally (or intentionally) coupled your API or UI to the database.  This means that often if you want to make a change to your table structure or persistence medium, you better be willing to shell out the time to fix the broken API clients or UI.

Armed with that thought, I went out to create an architecture that didn’t have this problem. I was dead-set on architecting a solution that ensured decoupling of the database and the clients of the API layer. I was going to develop an API that had a fluent business interface, and ensured that the persistence happened behind the curtains of the API.

The original design principle was dead simple. ‘Keep the persistence out of the API’. So there was a Business Logic Layer (BLL) which exposed a consumable API and housed the business and translation logic. Then a Data Access Layer (DAL) which housed the logic of storing the table classes. Classic separation of concerns. Classic.

OriginalArch

I went about my business of implementation. Then next thing I know!? This architecture worked, it performed, it was unit testable (mostly) and it was easy to understand! HooRay! Kinda.

To exemplify what I mean, here’s a canonical example of a bank transaction.

// Exposed via BLL DLL
public enum EntryTypes { Debit, Credit }

public class Transaction
{
    public Guid Id { get; }
    public Guid Account { get; }
    public DateTimeOffset Time { get; }
    public EntryTypes Type { get; }
    public double Value { get; }
}

public interface ITransactionManager
{
    Transaction Deposit(Guid account, double value);
    Transaction Withdraw(Guid account, double value);
}

public class TransactionManager : ITransactionManager
{
    IPersistence Persistence { get; }

   public Transaction Deposit(Guid account, double value)
   {
       // create a new transaction
       var transaction = new Transaction();
    
       // ...
       // translate the transaction to a transaction entry
    
       var entry = new TransactionEntry();
       // ... copy the fields
    
       Persistence.CreateTransactionEntry(entry);
       return transaction;
    }

    public Transaction Withdraw(Guid account, double value)
    {
       var account = Persistence.ReadAccountEntry(account);
       if( account.Balance < value )
            throw new InsufficientFundsException();
       // create a new transaction
       var transaction = new Transaction();
       // ...
       // translate the transaction to a transaction entry
       var entry = new TransactionEntry();
       // ... copy the fields
       Persistence.CreateTransactionEntry(entry);
       return transaction;
     }
}

// Exposed via DAL DLL
[Table("Transactions")]
public class TransactionEntry
{
    public Guid Id { get; }
    public Guid Account { get; }
    public DateTimeOffset Time { get; }
    public EntryTypes Type { get; }
    public double Value { get; }
}

[Table("Accounts")]
public class AccountEntry
{
    public Guid Id { get; set; }
    public string Owner { get; set; }
    public double Balance { get; set; }
}

public interface IPersistence
{
    void CreateTransactionEntry(TransactionEntry entry);
    AccountEntry ReadAccount(Guid accountId);
}

public Database : IPersistence
{
    public void CreateTransactionEntry(TransactionEntry entry)
    {
        // Code to store the table entry into a database.
    }
    // ...
}

Now, this example is contrived and won’t compile in any language but it illustrates my point. You can see from the client API, there is no mention of persistence. This means that the clients of the API don’t need to deal in classes that directly represent your tables (or know about them). I’ve intentionally let the two classes have identical fields, because this is just the reality for now. It leaves us the ability to change it in the future. If we ever need to change a table data type, add a column or change where the values get stored.

But there’s a problem with this architecture, it’s subtle (at least it was for me, experienced people are probably screaming ‘YOU IDIOT’) but it’s there. The fact that the BLL even knows about the table classes is a problem. There are a few things wrong:

  1. I used Entity Framework in this example. So I’ve basically just enforced my business logic to use Entity Framework. This isn’t really a bad thing, Entity is widely used, supported by Microsoft and easy to use (IMO). However, if you ever wanted to depart from Entity Framework, you’re going to have a bad time.
  2. I’ve coupled the Business Logic to the table columns (yet again). In this example, if you ever wanted to have different storage implementations, they better represent the data the same way across the implementations (or build a translation shim).
  3. I broke the Single Responsibility Principle on my BLL. Translating objects from API objects to database objects isn’t Business Logic so it’s got no ‘business’ being there.

Now, the saving grace of this architecture is that I did one thing right. I kept those details, the persistence details, away from the clients of the API. So even if I had released the API and had clients consuming, I wouldn’t break clients fixing my mistake.

As the architecture began to grow this oversight became more apparent. Which is always the case. I started to realize that things like porting to a different storage medium, or changing ORMs would pose a huge undertaking.

Another problem, since the only downstream interface coming out of the BLL utilized the table classes, all my tests were directly coupled to the table entries. I had a huge problem.

At this point I had already started reading “Clean Architecture” by Robert ‘Uncle Bob’ Martin. I started to understand where I had gone wrong, and those three issues noted above started to become clear, and so did a solution — Clean Architecture. Like I said, I stumbled across this architecture. More like tripped, fell, and face planted right into it.

Uncle Bob talks about a 4 ringed solution. In the centre ring, we have ‘Entities’ these are your ‘Enterprise Business Rules’. In the next ring, you have ‘Use Cases’ these are your ‘Application Business Rules’. Next, ‘Interface Adapters’ are your controllers and presenters. Then finally, ‘Frameworks and Drivers’ the external interfaces. He talks about never letting an inside ring, know about things in an outside ring. Can you see now my blunder?

Okay — so what would the point of this blog post be, if I didn’t have a little perspective on that architecture? Otherwise, I could just say ‘go read the book’.  Disclaimer: I’m not knocking Uncle Bob. I see the value in his architecture, and he’s got many many more years on me.

I think it can be simplified… I gave it a KISS. My interpretation is the ‘Wedding Cake Architecture’ and it looks like this.

WeddingCakeArch

It’s very very very heavily influenced by the Clean Architecture. Except that I’ve shaved out a layer. I’ve also intentionally drawn the layers with differing thicknesses, this is to illustrate the weighted dispersal of the code. You’ll have less code in the Business Models module, than you will down in the everything else module. You personally might not have much code in those layers, but if you imagine the sheer amount of coding behind Entity Framework, or a different ORM, or the ASP.NET Web API, you’ll see why that layer is so thick.

Here’s the example, reimagined:

// Exposed via BLL DLL
public enum EntryTypes { Debit, Credit }

public class Transaction
{
    public Guid Id { get; }
    public Guid Account { get; }
    public DateTimeOffset Time { get; }
    public EntryTypes Type { get; }
    public double Value { get; }
}

public interface ITransactionManager
{
    Transaction Deposit(Guid account, double value);
    Transaction Withdraw(Guid account, double value);
}

public class TransactionManager : ITransactionManager
{
    IPersistence Persistence { get; }

   public Transaction Deposit(Guid account, double value)
   {
       // create a new transaction
       var transaction = new Transaction();
       // setup transaction
       Persistence.CreateTransaction(transaction);
       return transaction;
    }

    public Transaction Withdraw(Guid account, double value)
    {
       var account = Persistence.ReadAccount(account);
       if( account.Balance < value )
            throw new InsufficientFundsException();
       // create a new transaction
       var transaction = new Transaction();
       // ...
       
       Persistence.CreateTransaction(transaction);
       return transaction;
     }
}

// Exposed via DAL DLL
[Table("Transactions")]
internal class TransactionEntry
{
    public Guid Id { get; }
    public Guid Account { get; }
    public DateTimeOffset Time { get; }
    public EntryTypes Type { get; }
    public double Value { get; }
}

[Table("Accounts")]
internal class AccountEntry
{
    public Guid Id { get; set; }
    public string Owner { get; set; }
    public double Balance { get; set; }
}

public interface IPersistence
{
    void CreateTransaction(Transaction entry);
    Account ReadAccount(Guid accountId);
}

public Database : IPersistence
{
    public void CreateTransaction(Transaction entry)
    {
        // translate the transaction to a transaction entry 
        var entry = new TransactionEntry(); 
        // ... copy the fields
        // Code to store the table entry into a database.
    }
    // ...
}

 

Now, the Business Logic Layer has zero knowledge of anything other than it’s own logic. So lets look at the benefits:

  1. Any clients of the API won’t know about the persistence (so I meet my design principle #1).
  2. We haven’t coupled our Business Logic to our persistence. So we have the flexibility to change our storage medium, or have different store implementations.
  3. Our Unit Tests will now be flexible to any of those changes. Not like before where the unit tests were directly coupled to the table classes. Before we had to use them to check proper output from the BLL.

Overall, I learned a lot from this little venture down Architecture Alley. I hope you can learn something from tale of the adventure too.

Happy Coding!

PL

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s