Speaker 1: It's Black Friday. You spent maybe an hour hunting down
Speaker 1: deals on this massive retail site and you finally, you know,
Speaker 1: you finally click check out.
Speaker 2: Oh yeah, the pressure's totally on, right.
Speaker 1: The pressure's on, and the shopping cart just freezes, or
Speaker 1: even worse, the page refreshes and just drops your items entirely.
Speaker 2: Which is just the worst feeling.
Speaker 1: It really is. If you're anything like me, you probably
Speaker 1: just stare at the screen and pure frustration.
Speaker 2: I mean, it is a totally broken digital promise, right,
Speaker 2: You're left completely stranded by a system that just couldn't
Speaker 2: handle the reality of, you know, a massive traffic spike.
Speaker 1: Yeah, exactly. And yet there are apps and systems out
Speaker 1: there that actually don't do this, systems that handle millions
Speaker 1: of users at the exact same time without even breaking
Speaker 1: a sweat.
Speaker 2: Yeah, they just keep humming along.
Speaker 1: So today our mission is to figure out the secret
Speaker 1: architecture behind those rock solid platforms. We are going to
Speaker 1: explore the concepts in a deep dive on functional and
Speaker 1: reactive domain modeling by Debasish Ghosh.
Speaker 2: Such a great source text, it really is.
Speaker 1: And the goal today is to give you the listener
Speaker 1: a bit of a shortcut to understanding how modern software
Speaker 1: is actually built by merging three really powerful ideas.
Speaker 2: Right, So that's domain driven design, functional programming, and reactive architecture.
Speaker 1: Exactly, Because I mean, while building massive global systems is
Speaker 1: inherently complex, the big secret in software engineering right now
Speaker 1: is that if we start treating our code a little
Speaker 1: bit more like pure.
Speaker 2: Mathematics, it can actually save us from the total chaos
Speaker 2: of the real world, which sounds crazy, but it's true.
Speaker 1: It does sound crazy. But before we can even start
Speaker 1: talking to pure math or you know, preventing server crashes,
Speaker 1: we kind of figure out what it is we are
Speaker 1: actually building, right. You need a blueprint, We need a blueprint,
Speaker 1: and in this architecture, that blueprint is called a domain model,
Speaker 1: which is built using domain driven design or DDD.
Speaker 2: Yeah, so think of a domain model as a direct
Speaker 2: translation of how a business actually works in the real world,
Speaker 2: but translated into software.
Speaker 1: Okay, makes sense.
Speaker 2: But to do that translation accurately, Domain driven design says
Speaker 2: you absolutely must start with what they call a ubiquitous language.
Speaker 1: Ubiquitous language meaning meaning everyone involved from the CEO to
Speaker 1: the junior developer speaks the exact same way.
Speaker 2: That's the core of it. Yeah, it means using actual
Speaker 2: business terms directly in the code itself. So if you
Speaker 2: are building a banking app, developers shouldn't be naming things
Speaker 2: with like generic programmer jurgen.
Speaker 1: Like user data object or whatever.
Speaker 2: Exactly, or transaction hash. You use the vocabulary of the domain.
Speaker 2: You use terms like debit, credit, portfolio account.
Speaker 1: Right.
Speaker 2: The real acid test is whether a business expert, like
Speaker 2: an actual working banker, could sit down, look at the
Speaker 2: code structure and theoretically understand the.
Speaker 1: Workflow because you eliminate that really dangerous translation layer between
Speaker 1: the business folks and the programmers, which.
Speaker 2: Is where so many bugs usually hide. To be honest,
Speaker 2: we totally.
Speaker 1: And to build this domain model we rely on a
Speaker 1: few specific building blocks, the main ones being entities and
Speaker 1: value objects.
Speaker 2: Yeah, and the distinction between those two is one of
Speaker 2: the most fundamental concepts in domain driven design. So let's
Speaker 2: look at entities first.
Speaker 1: Okay.
Speaker 2: An entity is an object that has a unique identity
Speaker 2: that never ever changes, even if its internal attributes due
Speaker 2: like a bank account. An account is a perfect example.
Speaker 2: It has a unique account number. You might change the
Speaker 2: name on the account, or you know, the balance will
Speaker 2: obviously fluctuate but it is still fundamentally the same account
Speaker 2: because the ID remains the same.
Speaker 1: Okay, let's unpack this for a second. An entity is
Speaker 1: like it's like a car with a VEN number.
Speaker 2: Oh I like that.
Speaker 1: Yeah, you can paint the car neon green, you can
Speaker 1: completely change the tires, you can swap out the radio,
Speaker 1: but legally, because of that gin, it is still the
Speaker 1: exact same car.
Speaker 2: That is a perfect analogy. Now contrast that car with
Speaker 2: a value object.
Speaker 1: Right.
Speaker 2: A value object has absolutely no identity of its own.
Speaker 2: It is defined entirely by its value, which means it
Speaker 2: is completely immutable. You cannot change it.
Speaker 1: So a value object would be like a twenty dollars bill.
Speaker 1: You don't care which exact twenty dollars bill you have
Speaker 1: in your wallet. I mean, you aren't tracking the serial number.
Speaker 1: You only care that the value is twenty dollars.
Speaker 2: Right, if you exchange it for two tens, you haven't
Speaker 2: altered your twenty dollar bill. You've swapped it for something
Speaker 2: entirely new, got it? Or think about a residential address
Speaker 2: in a banking system. An address is really just a
Speaker 2: collection of values street, city, zip code. If you change
Speaker 2: the zip code on an address, you haven't updated the.
Speaker 1: Address, You've created a completely different address exactly.
Speaker 2: Because value objects are defined by their values and can't
Speaker 2: be modified, they're incredibly safe. You can pass them around
Speaker 2: the system freely without worrying about keeping track of some
Speaker 2: unique ID or accidentally altering them.
Speaker 1: Okay, so we have the raw materials, the entities and
Speaker 1: the value objects, but they just sort of sit there, Right,
Speaker 1: how do we actually make them do something, like, say,
Speaker 1: transfer money between two accounts.
Speaker 2: That's where the third element comes in, which are services. Yeah,
Speaker 2: services are the higher level actions. They represent the actual
Speaker 2: behaviors where multiple entities and value objects interact.
Speaker 1: So like the actual transfer process.
Speaker 2: Right, a service would be the action of transferring funds.
Speaker 2: It's a coordinated operation that involves, you know, pulling from
Speaker 2: one account entity, adding to another account entity, and swapping
Speaker 2: out the money value objects inside them.
Speaker 1: The implication of that structure is really clean, but there
Speaker 1: is a nuance to it that caught my eye in
Speaker 1: the book. The definitions of these objects aren't actually universal,
Speaker 1: are they.
Speaker 2: No, not at all.
Speaker 1: They depend heavily on something called a bounded context.
Speaker 2: The bounded context is so crucial. The meaning of an
Speaker 2: object changes entirely depending on the specific module or context
Speaker 2: you are working in. Right, Take that address we just
Speaker 2: talked about. In our personal banking context, an address is
Speaker 2: just a value object. We only care about the text
Speaker 2: to print on a bank.
Speaker 1: Statement, right, just a string of letters and numbers.
Speaker 2: But imagine you are building a mapping application, like a
Speaker 2: GPS routing system.
Speaker 1: Oh wow, In that context, the address isn't just text
Speaker 1: on the page anymore.
Speaker 2: Exactly, in a mapping context, that same address suddenly becomes
Speaker 2: an entity. It gets its own unique idea in the database,
Speaker 2: it gets GPS coordinates, it gets a whole life cycle.
Speaker 2: Context totally dictates the design.
Speaker 1: So now we have our blueprint, we have our language,
Speaker 1: our context, and our objects. But if we go back
Speaker 1: to that Black Friday scenario.
Speaker 2: Freezing shopping cart.
Speaker 1: Yeah, how do these entities interact without the server just
Speaker 1: catching on fire when a million people log in at once?
Speaker 1: I think we need to talk about the shift from
Speaker 1: traditional object oriented design to functional programming. Yeah.
Speaker 2: To understand why that shift is happening across the industry,
Speaker 2: we have to confront the ghost in the machine of
Speaker 2: older architectures, which is mutable state.
Speaker 1: Mutable state.
Speaker 2: Right, for decades, the standard way to write software relied
Speaker 2: heavily on changing data in place. You have an object,
Speaker 2: say an account with a balance of one thousand dollars.
Speaker 2: When someone withdraws money, the code reaches into that exact
Speaker 2: object memory and mutates the balance down to nine hundred, which.
Speaker 1: Honestly, it sounds totally logical. You take money out, the
Speaker 1: number goes down.
Speaker 2: It does until you introduce concurrency. Think about Payday. Millions
Speaker 2: of users are accessing the same banking system simultaneously. If
Speaker 2: you rely on mutable state, you have an absolute concurrency nightmare.
Speaker 1: Because everyone is trying to change things at once.
Speaker 2: Imagine two separate server threads trying to access and mutate
Speaker 2: that exact same account balance at the exact same millisecond.
Speaker 2: Thread A tries to withdraw one hundred dollars, while THREADBE
Speaker 2: tries to deposit five hundred. Oh Now, if they're both
Speaker 2: trying to overwrite the state in place, they can collide,
Speaker 2: overwrite each other and cause massive data corruption.
Speaker 1: The system panics, the math gets wrong, and somebody loses
Speaker 1: a lot of money.
Speaker 2: Exactly, It is incredibly difficult to reason about code when
Speaker 2: the data is constantly shifting underneath you. So the functional
Speaker 2: programming approach takes a pretty radical stance. We kill the
Speaker 2: mutable state entirely. We completely separate state from behavior. State
Speaker 2: is stored in what are called algebraic data types or ADTs,
Speaker 2: which are strictly immutable. Behavior is handled separately by pure functions.
Speaker 1: Okay, let me stop you right there, because algebraic data
Speaker 1: types sounds incredibly intimidating. What makes a piece of data algebraic?
Speaker 2: I know it sounds super academic, but it just means
Speaker 2: you can struct your data types using the mathematical concepts
Speaker 2: of sums and products.
Speaker 1: Sums and products.
Speaker 2: Yeah, So, a product type is an A and D relationship,
Speaker 2: a user profile is a name and D an email
Speaker 2: and the a password that makes sense, and a sum
Speaker 2: type is an ore relationship. A payment method is either
Speaker 2: a credit card or a bank transfer or a gift card,
Speaker 2: oh icee. By defining data this strictly, the compiler can
Speaker 2: actually check your logic mathematically to ensure you've handled every
Speaker 2: possible scenario. And crucially, once these ADTs are created, they
Speaker 2: cannot be changed.
Speaker 1: Wait, hang on, I'm not letting that go. If I
Speaker 1: deposit one hundred dollars into my account and the account
Speaker 1: ADT is mathematically immutable, meaning the code literally cannot change it.
Speaker 1: How does my balance actually go up?
Speaker 2: It's a great question.
Speaker 1: I might have stuck with my old balance forever.
Speaker 2: No. No, The answer is that instead of mutating the
Speaker 2: existing account object, the function uses a smart constructor to
Speaker 2: generate a brand new copy of the account, but with
Speaker 2: the updated balance.
Speaker 1: I still don't get how that works at scale, though,
Speaker 1: How do you mean? Well, if Amazon is processing a
Speaker 1: billion transactions an hour and they have to generate a
Speaker 1: brand new clone copy of a massive shopping cart object
Speaker 1: every single time I add a single pair of socks,
Speaker 1: that has to blow up the servers. Oh right, The
Speaker 1: memory consumption would be astronomical. What is the code actually
Speaker 1: doing behind the scenes.
Speaker 2: You're touching on Honestly the biggest hurdle for people moving
Speaker 2: to functional programming. It would blow up the memory if
Speaker 2: it were doing a naive total clone, right, But it
Speaker 2: doesn't do that. It uses something called structural sharing.
Speaker 1: Structural sharing, how does that work?
Speaker 2: Think of your shopping cart data as a giant tree
Speaker 2: with many branches. You have branches for electronics, clothing, shipping details,
Speaker 2: and so on.
Speaker 1: Okay, if you.
Speaker 2: Add a pair of socks, the system doesn't copy the
Speaker 2: whole tree. It creates a new root of the tree,
Speaker 2: and it creates the one new tiny branch for the socks.
Speaker 1: Oh wow.
Speaker 2: But for all the other branches, the electronics, the shipping,
Speaker 2: the new tree just points back to the exact same
Speaker 2: branches from the old tree in memory.
Speaker 1: Oh so, because nothing in the old tree can ever
Speaker 1: be mutated, it's perfectly safe for the new tree to
Speaker 1: share those older parts exactly.
Speaker 2: It's incredibly memory efficient. You are only allocating memory for
Speaker 2: the tiny piece that actually changed.
Speaker 1: That is brilliant.
Speaker 2: But the massive benefit is concurrency. Because that old tree
Speaker 2: is never overwritten, it remains perfectly safe. Any number of
Speaker 2: concurrent threads can be reading that old state at the
Speaker 2: exact same time without any fear of data corruption. The
Speaker 2: whole fear of concurrency completely vanishes.
Speaker 1: Because nobody is fighting over the same chock. They all
Speaker 1: get their own brand new chalkboard and they just share
Speaker 1: the erasers.
Speaker 2: I love that.
Speaker 1: Yeah. So, because our data is totally immutable, something really
Speaker 1: magical happens to the functions themselves. They start acting like
Speaker 1: high school algebra.
Speaker 2: Yes, and this brings us to a concept called referential transparency.
Speaker 1: Which sounds intense.
Speaker 2: Again, it's simpler than it sounds, I promise. Think back
Speaker 2: to basic math y equals f of x. If your
Speaker 2: function is to square a number and you input three,
Speaker 2: the output is always nine.
Speaker 1: Right.
Speaker 2: It doesn't matter how many times you run it, what
Speaker 2: time of day it is, or what other equations are
Speaker 2: running next to it. A pure function will always yield
Speaker 2: the exact same output for the same input, and crucially,
Speaker 2: it has no secret side effects.
Speaker 1: Side effects like secretly writing a log to a database,
Speaker 1: or throwing an unhandled exception that crashes the program, or
Speaker 1: changing some global variable exactly.
Speaker 2: A pure function does nothing except take an input and
Speaker 2: return an output.
Speaker 1: And because there are no side effects, you get the
Speaker 1: substitution model. You can mentally or programmatically just replace the
Speaker 1: function call with its output. Right.
Speaker 2: If I see square of three in the code, I
Speaker 2: can literally substitute it with nine, making equational reasoning possible.
Speaker 2: You can logically prove your code works.
Speaker 1: The implication here is massive for building large architectures, because
Speaker 1: if your functions are pure mathematical expressions, they give you
Speaker 1: the ultimate superpower, which is compositionality. You can literally snap
Speaker 1: small functions together to build larger behaviors like fusion exactly.
Speaker 2: Say you need to map a list of accounts to
Speaker 2: calculate their interest, and then map that list again to
Speaker 2: deduct taxes. Okay, because those functions are pure, you can
Speaker 2: algebraically fuse them together into a single pass. You calculate
Speaker 2: the interest and deduct the tax simultaneously, which really optimizes performance.
Speaker 1: Here's where it gets really interesting to me. Think of
Speaker 1: compositionality like a massive plumbing system.
Speaker 2: Okay, plumbing.
Speaker 1: If you have a bunch of pure functions, they act
Speaker 1: like different pieces of pipe. If the types align, like
Speaker 1: if the pipe sizes matge exactly, you can snap these
Speaker 1: functional pipes together to create a massive system without needing
Speaker 1: to know anything about the actual water flowing inside them.
Speaker 2: Oh that's good.
Speaker 1: You define what you want to happen, and you completely abstract.
Speaker 2: A way to how that analogy holds up perfectly. You
Speaker 2: are assembling behaviors based purely on their algebraic contracts.
Speaker 1: But pure math assumes a perfect world, a white room
Speaker 1: where everything works perfectly. The real world is chaotic. Databases
Speaker 1: get unplugged, hard drives get full, network connections drop out
Speaker 1: in the middle of a transaction. How do we protect
Speaker 1: our beautiful, pure algebra from the messy reality of the internet.
Speaker 2: Well, this is where we bring in the third pillar,
Speaker 2: which is reactive architecture. Pure logic is great, but software
Speaker 2: has to actually survive the real world. A reactive model
Speaker 2: relies on a specific three plus one view To achieve
Speaker 2: this three plus one yeah. The ultimate goal is that
Speaker 2: a system must be responsive.
Speaker 1: Meaning bounded latency, like the user gets an answer quickly
Speaker 1: no matter what.
Speaker 2: Yes. But to guarantee responsiveness, it must be backed by
Speaker 2: three interlocking treits. First, it must be resilient, meaning it
Speaker 2: responds gracefully to failure.
Speaker 1: Okay.
Speaker 2: Second, it must be elastic, meaning it scales up and
Speaker 2: down based on varying loads. And to achieve both resilience
Speaker 2: and elasticity, the foundation must be message driven, relying on
Speaker 2: asynchronous communication.
Speaker 1: So we have to actively design for failure, which honestly
Speaker 1: contradicts how a lot of legacy systems were built.
Speaker 2: It does, but at scale, when you have ten thousand servers,
Speaker 2: hardware will fail multiple times a day. It is a
Speaker 2: mathematical certainty.
Speaker 1: You just can't avoid it.
Speaker 2: You don't pretend failures won't happen. You design the architects
Speaker 2: are assuming they're happening right now.
Speaker 1: But how does that work with our pure functional math,
Speaker 1: Because in traditional code you see those messy tricatch blocks everywhere.
Speaker 1: You know, try to write to the database. If it fails,
Speaker 1: catch the error. But earlier you said throwing exceptions breaks
Speaker 1: referential transparency. It's a secret side effect that basically ruins
Speaker 1: the math.
Speaker 2: It does. A tricatch block absolutely destroys the purity of
Speaker 2: a function. The function is no longer just returning an
Speaker 2: output based on an input. It's blowing up the execution stack.
Speaker 2: So instead of mixing messy exception handling into our business logic,
Speaker 2: functional programming meets reactive architecture by abstracting failures and latency
Speaker 2: into actual data types. We call these monads monads.
Speaker 1: Okay, how do they solve the trycatch problem?
Speaker 2: Think of a monad as a special wrapper or a
Speaker 2: shipping box. Instead of a function directly returning an account balance,
Speaker 2: it returns a box called try, a box called try Yeah.
Speaker 2: Inside that trybox is either the successful balance or the
Speaker 2: failure exception. The function itself never crashes. It always safely
Speaker 2: returns the box.
Speaker 1: And if the network is just really slow, then.
Speaker 2: We use a box called a future. It essentially says,
Speaker 2: you know, I don't have the balance right now because
Speaker 2: the network is slow, but here is a future box.
Speaker 2: Keep executing the rest of the code, and I'll drop
Speaker 2: the result in here when I'm done.
Speaker 1: Oh wow, That allows the main thread of the application
Speaker 1: to just keep moving. The user isn't staring at a
Speaker 1: frozen screen because the main thread isn't blocked waiting for
Speaker 1: a slow database exactly. But I do have a problem
Speaker 1: with this. If we remove all that failure handling from
Speaker 1: our pure business logic and we centralize it into a
Speaker 1: separate failure handling module, doesn't that just create a massive bottleneck,
Speaker 1: like you know, one traffic cop trying to manage all
Speaker 1: the crashes for a whole city.
Speaker 2: If you used a single global error handler, Yes, the
Speaker 2: system would bottleneck immediately.
Speaker 1: Okay, so how do they avoid it?
Speaker 2: The reactive architecture avoids this by delegating those handlers based
Speaker 2: on the specific bounded context we talked about.
Speaker 1: Earlier, ah tying it back to the domain blueprint precisely.
Speaker 2: The portfolio context has its handler, the transaction context has
Speaker 2: its own. Treating exceptions and latency as manageable effects in
Speaker 2: their own specific contexts allows our pure functional core to
Speaker 2: scale elastically, so it's modular right. The system can spin
Speaker 2: up more servers for just the specific context that is
Speaker 2: struggling without bringing down the whole application.
Speaker 1: To be truly asynchronous and message driven. Like you mentioned
Speaker 1: in the three plus one view, it feels like we
Speaker 1: have to rethink how our system fundamentally communicates. And I
Speaker 1: see developers throw around the terms commands and events all
Speaker 1: the time, almost interchangeable now all the time. But in
Speaker 1: this architecture they aren't the same thing, are they.
Speaker 2: They are drastically different, and understanding that difference is key
Speaker 2: to a message driven system. A command is an instruction.
Speaker 2: It is an imperative request like debit account, because it
Speaker 2: is a request to change the state of the system
Speaker 2: in the present or future. It can be rejected the
Speaker 2: account might be empty. A command can fail in an event.
Speaker 2: An event is a notification of a past change debit occurred.
Speaker 2: It is a historical fact because it already happened. It
Speaker 2: is completely immutable. You cannot reject.
Speaker 1: History, so an event cannot fail exactly.
Speaker 2: An event cannot fail, and.
Speaker 1: This distinction leads us to one of the most powerful
Speaker 1: concepts in modern system design, which is yes.
Speaker 2: Think about how traditional databases work. They store the current state.
Speaker 2: Let's look at the example of a customer named Bob
Speaker 2: Okay Bob. In nineteen eighty nine, Bob opened an account
Speaker 2: and lived at address A. In twenty ten, Bob moved
Speaker 2: to address B. In a traditional database, you would execute
Speaker 2: an update command and overwrite address A. With address B.
Speaker 1: You literally erase the history to store the present you do.
Speaker 2: But in an event source system, you don't overwrite anything ever. Instead,
Speaker 2: the system logs a completely immutable event address.
Speaker 1: Change, so it just adds it to a list.
Speaker 2: Right, the database is no longer just a snapshot of
Speaker 2: the current moment. It is a permanent, append only historical
Speaker 2: log of every single thing that has ever happened in
Speaker 2: the system.
Speaker 1: Wait, if I want to know where Bob lives right now,
Speaker 1: does the system have to read his entire forty year
Speaker 1: life history just tofind his current zip code?
Speaker 2: Theoretically yes. If you take the initial empty state of
Speaker 2: the system at times zero and you mathematically apply every
Speaker 2: single event in the log sequentially, you will arrive at
Speaker 2: the exact current snapshot.
Speaker 1: That's crazy.
Speaker 2: The entire history of your business model boils down to
Speaker 2: one long mathematical equation that.
Speaker 1: Is essentially time travel. I mean, if a catastrophic bug
Speaker 1: corrupts the current state of the database, you don't panic.
Speaker 1: You just wipe the corrupted current state, go back to
Speaker 1: the event log, and replay the math right up to
Speaker 1: the second before the bug hit.
Speaker 2: It makes the system incredibly resilient, and practically systems take
Speaker 2: periodic snapshots so they don't have to read forty years
Speaker 2: of history every single time you log in. Oh, that
Speaker 2: makes sense, but the ultimate source of truth is always
Speaker 2: that event log. It completely aligns with the functional programming
Speaker 2: ideal of immutable data. You never mutate the past, you
Speaker 2: only append new facts to the log.
Speaker 1: Let's step back and synthesize all this. Yeah, because we
Speaker 1: have covered some serious architectural ground today, we really have.
Speaker 1: What we have learned from diving into this domain modeling
Speaker 1: approach is that functional programming gives us predictability and the
Speaker 1: safety pure mathematical logic. But pure logic isn't enough on
Speaker 1: its own. Reactive programming takes that pure logic and gives
Speaker 1: it a shield to survive the real world, keeping it responsive, elastic,
Speaker 1: and resilient to failure by passing around boxes like try
Speaker 1: and future instead of.
Speaker 2: Crashing, and by wrapping all of that within domain driven design,
Speaker 2: using the ubiquitous language of the business, you end up
Speaker 2: with software that accurately reflects the real world and can
Speaker 2: scale globally without collapsing under its own weight.
Speaker 1: So what does this all mean for us, the people
Speaker 1: actually using these systems? I want to leave you with
Speaker 1: a final concept to mull over on your own.
Speaker 2: Go for it.
Speaker 1: We just talked about event sourcing. If the most resilient
Speaker 1: modern software is shifting away from just storing our current
Speaker 1: status and instead is moving toward building an immutable, permanent
Speaker 1: log of every single event and action we have ever taken,
Speaker 1: how does an architecture that is designed to remember everything
Speaker 1: forever impact our digital privacy and basically the human right
Speaker 1: to be forgotten? Wow?
Speaker 2: That is a heavy thought.
Speaker 1: It's something to think about the next time you update
Speaker 1: your address online, or the next time you are sitting
Speaker 1: on a retail site on Black Friday and the cart
Speaker 1: actually works perfectly processing your order instantly. You'll know it's not.
Speaker 2: Magic, it's just really really good math exactly.
Speaker 1: Thank you for joining us on this deep dive. We'll
Speaker 1: catch you on the next one.