DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Using Java Class Extension Library for Data-Oriented Programming - Part 2
  • Using Java Class Extension Library for Data-Oriented Programming
  • Java 23: What Developers Need to Know
  • Using Lombok Library With JDK 23

Trending

  • Secrets Sprawl and AI: Why Your Non-Human Identities Need Attention Before You Deploy That LLM
  • Can You Run a MariaDB Cluster on a $150 Kubernetes Lab? I Gave It a Shot
  • Cloud Security and Privacy: Best Practices to Mitigate the Risks
  • Why Database Migrations Take Months and How to Speed Them Up
  1. DZone
  2. Coding
  3. Languages
  4. Event-Driven Fractals

Event-Driven Fractals

Functional programming can help making beautiful programs. It can feel familiar to everyday programming concepts, and there's no need to use unknown jargon.

By 
Hossein Naderi user avatar
Hossein Naderi
·
Dec. 19, 22 · Analysis
Likes (1)
Comment
Save
Tweet
Share
4.3K Views

Join the DZone community and get the full member experience.

Join For Free

Close-up of a Romanesco broccoli.

Close-up of a Romanesco broccoli

Message-passing applications are one of the main components of reliable distributed systems; above all, they make it feasible to decouple the "when" and "where" of a problem from "who does it how." Event-driven applications go one step further and give you the causal chain of your system as a first-class citizen.

While the difference between an event-driven system and one that’s not event-driven is pretty clear and obvious to most software engineers, there are a gazillion ways to design such a system. Each has its own trade-offs and is useful in a specific context. The one I’m going to talk about in this article is modeling applications as state machines that compose and form a fractal-like structure — as each component is an application, and composed ones are also the same kind of application.

For doing that, I’ll use Edomata as an example, a purely functional library in Scala that I wrote with this idea. But the main idea is general and can be implemented in any language with idiomatic considerations in the host language.

Why?

Besides their intuitive and easy-to-understand nature, state machines are one of the building blocks of our understanding of systems in general, and have been studied widely in several fields such as mathematics, distributed systems, hardware design, and software engineering, to name a few.

This feature comes to help us in expressing our ideas and models eloquently, while preserving its links to the real world so that we can implement it easily. Moreover, the composition feature helps us to divide and conquer the problem. We can understand a small state machine; we can understand how its composition behaves — so we can build larger state machines that we can understand, but couldn't have understood easily otherwise.

Event-Driven State Machines

Generally speaking, state machines consist of these elements:

  • State: Possible states (obviously)
  • Input: Possible input
  • Initial state
  • Transition: A function that determines how a state machine changes its state

A state machine is event-driven if the transition from one state to another is triggered by an event.

For example, if we define our transition function as fold-over events, we’ll get an event-sourced application! On the other hand, if we emit events between transitions, those events can be used for integration with other state machines! And that’s the idea of event-driven architecture.

Primitive State Machines

Let’s see how one of the primitive state machines in Edomata works. It’s named Decision and models pure event-sourced programs. It works as follows:

  • Initially, it is indecisive and contains a result value.
  • It can decide on some logic, then accept or reject.
  • If accepted, it must have one or more events (as it models event-sourced applications) and a result value.
  • If rejected, it must have one or more errors.

How Does It Compose?

A Decision contains a result value if it’s not rejected, so we can define a .map function that takes a function and returns a new Decision whose result value is the result of mapping using the provided function. The resulting Decision is also a Decision obviously, which is similar to how we started.

We can also bind two Decisions together. So we have Decision A that may contain a result value a, and we can provide a function that takes a and returns a new Decision B. We get a new Decision that is the result of composition and behaves like the following:

state A state B result
accepted accepted events a + events b, value b
accepted indecisive events a, value b
indecisive accepted events b, value b
* rejected no events, no value, errors b
rejected * no events, no value, errors a

This shows that errors cause short circuits, and also, events do accumulate.

The interesting property is its self-similar structure (like a fractal) that lets us encapsulate some logic behind a state machine, which may be the result of composition of several other state machines.

But it does not end here.

Compound State Machines

That was an example of a simple primitive state machine. State machines can also embed other state machines and add new behaviors on top of other state machines. That’s what an Edomaton does, for example; it models general event-sourced applications and can do the following:

  • Read state
  • Run an idempotent side effect
  • Decide (using Decision)
  • And publish external events (note that in well-designed systems, event-sourcing is an internal implementation detail and is hidden from outside systems, and journal is not meant for integration; that’s why there’s a separate event channel here)

How Does It Compose?

It behaves exactly like Decision, but has these new capabilities:

  • External events are also accumulated if not rejected
  • External events get reset, when rejected
  • Also, it can recover a rejected decision by publishing external events, so it can be used as some form of compensation event

Again, we see a self-similar structure with all the properties we’ve talked about before. Programs written this way are truly fractal-like structures, created from little elements that are all the same fractal!

Koch snowflake

In Action

The following is an example from the Edomata tutorial. As you can see, it’s a very simple code that manipulates data in memory. No magic or supernatural phenomena found in most of the frameworks! (But hey, that’s a library.)

Scala
 
enum Account {
  case New
  case Open(balance: BigDecimal)
  case Close
  
  def open : Decision[Rejection, Event, Open] = this.decide { 
    case New => Decision.accept(Event.Opened)
    case _ => Decision.reject(Rejection.ExistingAccount)
  }.validate(_.mustBeOpen)
  
  def close : Decision[Rejection, Event, Account] = 
    this.perform(mustBeOpen.toDecision.flatMap { account =>
      if account.balance == 0 then Event.Closed.accept
      else Decision.reject(Rejection.NotSettled)
    })
  
  def withdraw(amount: BigDecimal): Decision[Rejection, Event, Open] = 
    this.perform(mustBeOpen.toDecision.flatMap { account => 
      if account.balance >= amount && amount > 0 
      then Decision.accept(Event.Withdrawn(amount))
      else Decision.reject(Rejection.InsufficientBalance) 
    }).validate(_.mustBeOpen)

  def deposit(amount: BigDecimal): Decision[Rejection, Event, Open] = 
    this.perform(mustBeOpen.toDecision.flatMap { account =>
      if amount > 0 then Decision.accept(Event.Deposited(amount))
      else Decision.reject(Rejection.BadRequest)
    }).validate(_.mustBeOpen)
  
  private def mustBeOpen : ValidatedNec[Rejection, Open] = this match { 
    case o@Open(_) => o.validNec
    case New => Rejection.NoSuchAccount.invalidNec
    case Close => Rejection.AlreadyClosed.invalidNec
  }
}


Testing

One of the nicest properties of state machines we’ve not talked about yet is that state machines are not the machine — they are the definition of the machine! So we can run a state machine in any state easily, and that’s a high win in testing. It allows us to easily use property testing on our business logic, which shows how high it can reach in level of testing.

Scala
 
Account.New.open
// res1: Decision[Rejection, Event, Open] = Accepted(Chain(Opened),Open(0))
Account.Open(10).deposit(2)
// res2: Decision[Rejection, Event, Open] = Accepted(Chain(Deposited(2)),Open(12))
Account.Open(5).close
// res3: Decision[Rejection, Event, Account] = Rejected(Chain(NotSettled))
Account.New.open.flatMap(_.close)
// res4: Decision[Rejection, Event, Account] = Accepted(Chain(Opened, Closed),Close)


Running

Running such a system in production is trivial too. The only thing required is a back end capable of running state machines. It shows how much our business logic is decoupled from infrastructure concerns and implementation, which is also a high win!

Scala
 
val pool : Resource[IO, Session[IO]] = ??? // postgres connection pool

val buildBackend = Backend
  .builder(AccountService)
  .use(SkunkDriver("domainname", pool))
  .persistedSnapshot(maxInMem = 200)
  .withRetryConfig(retryInitialDelay = 2.seconds)
  .build


// Now you have a production ready system!

val application = buildBackend.use { backend =>
  val service = backend.compile(app)
  // compiling your application will give you a function
  // that takes a messages and does everything required,
  // and returns the result.

  // Now we can send a command to our service!
  service(
    CommandMessage("cmd id", Instant.now, "aggregate id", "payload")
  ).flatMap(IO.println)
}


Other Examples

There are other kinds of state machines implemented in Edomata, such as ResponseT (which models an event-driven program response), Stomaton (which models raw event-driven state machines, used in a CQRS style), Action (impure event-sourced applications), and a few others.
For more examples and detailed content, visit the project’s website and docs.

Also feel free to start discussions or submit issues on GitHub.

Conclusion

You might say: Well, this is the definition of monad. What’s new?

I would say yes! But calling it a monad wouldn’t make it more useful to people, would it? Most software engineers and developers today are not category theory experts, nor do they want to be so.

But this doesn’t mean they shouldn’t benefit from the plethora of useful and extremely helpful ideas found in these topics in action. Edomata is a library designed with this mindset; it can help you develop your event-sourced or CQRS applications rapidly, and it lifts the burden of unneeded complexity for you. It is extremely modular and can be made to work as you wish, as it’s a library, not a framework.

It can also be a good example of an un-opinionated design for these kinds of systems that is portable to other ecosystems too.

I would like to know if you find this interesting or have any kind of questions or feedback. Let me know in the comments, or let's have a discussion on GitHub!

Library Scala (programming language) Event-driven architecture Java (programming language)

Opinions expressed by DZone contributors are their own.

Related

  • Using Java Class Extension Library for Data-Oriented Programming - Part 2
  • Using Java Class Extension Library for Data-Oriented Programming
  • Java 23: What Developers Need to Know
  • Using Lombok Library With JDK 23

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!