00:00:20.000
We're a little behind. So, uh, welcome. Thank you for coming. Uh, my name is Alan Ridleh Hoover. My pronouns are he,
00:00:25.920
him, and I use uh excuse me, I'm a senior engineering manager at Cisco Mari.
00:00:34.000
Hi everyone, I'm Fito Fonastro. I also use a pronounce he him and I'm a staff software engineer also at Cisco Maraki.
00:00:43.440
There we go. Okay, so we call this talk derailing our application, but that's a little bit of a misnomer. We're not
00:00:49.760
actually removing Rails from our app. Rather, we're decoupling our business logic from Rails constructs because we
00:00:56.640
found that Rails was both getting in our way and not providing enough structure at our scale. Now, before we dive into
00:01:03.280
that though, there are a few things we need to kind of establish up front, some core concepts and some common language.
00:01:11.360
The first is a quick lesson in economics about convex and concave payouts. I promise this is only going to take a
00:01:17.360
minute. Um, a convex payout is one that
00:01:22.960
only requires a small investment uh and usually results in a small loss but
00:01:28.080
occasionally pays off really big. This is venture capital. This is you making
00:01:33.360
lots of small little investments in lots of small little experiments to see if any of them are going to pay off.
00:01:40.079
The opposite of that is a concave payout. This is one that requires a large investment and usually results in
00:01:47.360
a small reliable profit like banking. Uh but it can result in a very large loss.
00:01:54.000
I'm sure we all remember 2008. Um this is also kind of like retirement. Um you
00:02:00.479
have a nest egg, you want it to grow, but you can't afford to take any losses. So you plan carefully and you invest
00:02:07.119
conservatively. All right. Does everybody have those two concepts, convex and concave? Yeah. All
00:02:14.080
right, cool. Uh, I can't do a show of hands because I'm blinded, by the way.
00:02:19.680
Um, okay. So, that's the end of the economics lesson. Now, let's see if we can apply that concept to software
00:02:25.840
development. So, there's this guy, his name is Kent Beck. You may have heard of him. Uh, and
00:02:32.400
he has this talk called 3X. uh and in the talk he shows you how the convex and
00:02:37.760
concave payout curves are actually a single curve. Together they form an
00:02:43.280
S-shaped curve that represents the application development life cycle and three phases including exploration,
00:02:50.720
expansion, and extraction. In the explore phase, you're in the
00:02:56.000
convex portion of the curve. The goal here is to find the thing that's going to pay off really big.
00:03:02.319
In the expand phase, it's all about growth. You found that big thing. It's taking off and now your job is to scale
00:03:09.920
it as fast as you can. And then finally, once growth slows to a
00:03:15.280
reasonable like understandable amount and you've accumulated a large customer base whom you no longer can afford to
00:03:22.159
lose, the application enters what's called the extraction phase. Here you've
00:03:27.360
reached that concave portion of the curve and your goal is to eek out as much value as possible without risking
00:03:34.799
any losses. So knowing where you are on that 3x
00:03:40.400
application development life cycle can actually help you make decisions uh about what uh what to do with your
00:03:48.000
application. And so where are we on this curve? Well, when we started putting
00:03:54.080
together this talk, Theo and I thought, "Wow, we're probably in the expan in the extract phase." Um, but no, we did some
00:04:02.159
research. We looked into the data and it's pretty clear we are still expanding. Um, and to understand the
00:04:08.480
challenges that we're facing and why we're decoupling ourselves from Rails, let's take a look at how we got here to
00:04:13.840
this point. So, Moroi was founded in 2006, just 11 days after Twitter. We
00:04:20.639
were a startup with a crazy idea that that for a service that no one else was providing, remote network management
00:04:27.040
over the cloud. So you can set up a device and manage it using a website
00:04:32.960
instead of having to plug into it. So we set about exploring this space. It was completely uncharted territory.
00:04:40.080
Rails 1.0 had just shipped in December of 2005 and we were founded in April of
00:04:45.120
2006, which is when we started work on our dashboard. Now, at that point,
00:04:50.320
people were still writing fat controllers. The blog post hadn't been published yet. No one knew the Rails
00:04:56.240
wave. Hadn't been published. We hadn't yet lived through the Ruby 1 N upgrade. I don't know how many of you remember
00:05:01.759
that fiasco. A lot of you. Um, there were no restful routes. You couldn't
00:05:08.639
chain active record queries and bundler didn't exist yet. So, you had to do dependent dependency management
00:05:14.400
manually. Plus, we weren't well-grounded, eloquent, or practical.
00:05:22.320
But that didn't stop Cisco from acquiring Moro in December of 2012
00:05:27.600
because they recognized that Moroi was innovating incredibly fast and that despite that's despite the fact that the
00:05:34.639
central concepts in our application were laid down well before any of those best
00:05:39.680
practice practices were established. So by today's standards, our code was kind
00:05:44.960
of a mess. That's not an actual class diagram, but it's not far off.
00:05:50.800
So we just been acquired. All of a sudden, we had all these new customers coming to us who wanted products and
00:05:57.280
features we didn't have yet. Um, and so our our Rails app grew a whole bunch
00:06:03.039
despite the mess. uh we went on into full-on expansion mode
00:06:09.759
at the end of 2012 when we were acquired. There were 38 38 active
00:06:15.440
contributors to the codebase and we had about 250,000 lines of Ruby. Three years
00:06:21.039
later, at the beginning of 2016, there were twice as many contributors and over half a million lines of Ruby code. Then
00:06:28.960
three years later, which is about the time f Theo and I joined in 2019, the engineering team had doubled again
00:06:34.960
and there were nearly a million lines of Ruby code. Three years after that, there
00:06:40.880
were 250 monthly contributors and over 1.7 million lines of Ruby code.
00:06:47.360
And then Cisco decided in 2022 that all of Cisco's most powerful
00:06:55.280
devices called our Catalyst series, these are the $100,000 switches that telos buy. All of those devices would be
00:07:02.880
manageable via the Moroi dashboard. Um, which meant we needed to add several
00:07:08.000
hundred new products to our website. Uh so in the three years since then monthly
00:07:14.400
act active contributors have doubled again more than we're over 600 uh at
00:07:19.440
this point and our codebase is now 4.2 million lines of Ruby in just that short
00:07:26.319
time. So during the explore phase we built the
00:07:33.680
central concepts of our application without the benefits of modern Rails best practices. By today's standards,
00:07:39.120
the code was pretty messy. But at only a quarter of a million lines of code, it was probably pretty
00:07:45.280
manageable for those 40 people. Early in the expansion phase, we saw significant growth in both our codebase
00:07:51.759
and our team due to the constant barrage of new feature requests. The goal in the
00:07:57.280
code ba the growth in the codebase led to some astonishingly large models and controllers.
00:08:02.639
um that caused complexity to increase, making the code harder to understand,
00:08:07.680
harder to change, and harder to test. And without adequate test coverage, we were unable to move as quickly and as
00:08:14.160
confidently as we desired. Furthermore, because the models and controllers were shared across the
00:08:20.080
organization, no one individual or team could act to improve the code without
00:08:26.080
gaining consensus across the organization or imposing their solution
00:08:31.840
on everyone else. So, we wanted to increase our velocity
00:08:36.880
as we continue to scale. And to do so, we needed we felt like we needed to look beyond the MVC pattern that Rails
00:08:42.959
architecture supports in order to reduce the complexity and increase the testability of our application.
00:08:50.000
We also felt like Rails wasn't giving us enough guidance about how to structure our application to support a team at our
00:08:56.160
scale. We needed to change our approach in order to address these issues. FTO,
00:09:01.680
would you like to explain how? My pleasure. Right.
00:09:07.440
I'm clicking impair so I'm actually just going to use the keyword but um all right
00:09:13.760
so like Alan mentioned we wanted to reduce complexity and increase the stability so we chose to modularize our
00:09:21.120
codebase in order to address those two concerns and we also wanted to improve autonomy and accountability and so we
00:09:29.200
added boundaries within our monolith to support these qualities now let's look
00:09:34.320
at how we're applying these concepts to our Rails app. Okay. The first thing we did was we
00:09:40.880
wanted to teach people how to remove the business logic from our controllers. Now, this is modularization. You're
00:09:47.120
taking big things and you're breaking them into smaller things. So, we broke our large controller actions into much
00:09:53.760
smaller objects. Now, we started there for a couple of reasons. First, our business logic is the most
00:10:01.040
valuable code we have. It's really what differentiates us from our competitors and it's also where developers are
00:10:06.560
spending most of their time adding features and fixing bugs. So that's where we get the most or the biggest
00:10:12.640
return on investment from having high test coverage. Second, controllers can be really
00:10:19.360
difficult to test. They often require complex setups in order to just exercise
00:10:24.800
every code path in a controller action. And the more logic you have in that
00:10:30.240
action, the more cumbersome testing becomes. And eventually people just stop testing or opt for manual testing
00:10:38.320
instead, which seems easier, but it's actually slower and more errorprone.
00:10:45.200
So we restructure our code to make it easier to test via simpler, quicker unit
00:10:50.720
tests that require less setup. All right, we're going to look at an example. Um, now the code in these
00:10:57.360
examples is kind of made up. It was actually
00:11:02.480
inspired by real production code. It was transformed to use uh concepts related
00:11:09.519
to coffee machines instead of networking with maybe some help from AI. The shape
00:11:14.720
of the code is inspired by production. All right. This is a controller that
00:11:21.519
handles requests to update the settings of multiple coffee machines that belong to a cafe. Now, don't feel like you need
00:11:28.640
to understand the code. Instead, I want you to focus on the shape of the code and the variety of
00:11:35.200
colors that you see on the screen. Both of those at a glance are indicators that there are lots of different concepts
00:11:41.279
here, tons of things going on. So, I'm going to walk you through some highle things that are happening here.
00:11:48.720
There is an active record transaction which contains a loop and this is
00:11:54.320
looping over an array of arrays that contains a coffee maker ID and the
00:11:59.680
corresponding changes to that machine's configuration. Now inside the loop is where most of the
00:12:05.680
business logic lives. The code here is doing things like validating input or
00:12:10.959
populating errors, serializing a hash of changes, and finally saving these
00:12:17.040
settings. Now, back outside the loop, there's a little bit more business logic. The
00:12:22.560
controller is creating a cafe event, which uh is probably consumed by a different part of the system,
00:12:28.639
potentially some kind of notification system. And then finally, know there's so much
00:12:35.200
going on here. So for good measure, we add some aggressive exception handling and an insure block in charge of
00:12:41.360
rendering a response. Now again, there's a lot going on here, but we don't have to solve all these
00:12:47.519
problems at once. In fact, our approach has been to first focus on the body of
00:12:52.959
the loop. Now, some caveats here before we go there. It is important to ensure that
00:12:59.360
you have at least some highlevel tests. You just want to have enough coverage to tell you that uh you're not completely
00:13:06.320
breaking the method. So assuming you have that and that can be hard but assuming you have that uh we can get
00:13:13.279
started with the extraction. All right. So here we've extracted a class called update water tank settings.
00:13:21.680
This class encapsulates the procedure that was inside the loop and is now only concerned with a single coffee maker.
00:13:29.600
Now, this makes testing the logic much easier since you no longer have to think about HTTP concepts. You also don't have
00:13:37.040
to think about the loop. You can just focus on the effects of this class um
00:13:42.079
that it has on the settings of a single coffee machine.
00:13:47.360
But there's still a lot of logic left in the controller. So the next step is to
00:13:52.560
extract the remaining business logic out into another class.
00:13:57.600
So here we've created a new class. Um we called it update coffee machine settings and move the rest of the business logic
00:14:04.399
there. And if you look at the controller, it now looks like a whole lot more like a scaffolded controller.
00:14:10.480
It is only responsible for handling the HTTP request and sending the response back. Now, it collaborates though with
00:14:17.600
the new class that we just extracted to build the response.
00:14:22.720
Now, this new class on the other hand still has several responsibilities and admittedly a lot more than we'd like,
00:14:29.680
but we're going to defer that work to the next person that touches this file because our goal here is not to fix
00:14:36.800
everything at once. It's just to make it easier to test our business logic. And
00:14:42.079
so, now that we took that out of the controller, we've accomplished that goal. So we can move on for now.
00:14:48.639
Now as I mentioned earlier, extracting business logic out of the controller will help us with simplicity and
00:14:54.160
testability. But these extractions alone are not enough to promote autonomy and
00:14:59.760
accountability. For that we need some walls within our codebase.
00:15:05.600
And these boundaries are going to reduce the cognitive load across the entire engineering organization.
00:15:12.160
An engineer now no longer needs to know about all four million lines of code in order to make a change or fix a bug.
00:15:18.959
Rather, they can just focus on their code on the code their team owns. And they're free to improve that code
00:15:26.000
without having to gain organizational consensus. So we started with all the code in the
00:15:33.199
controller. So that was in the controller directory. And now we've extracted classes and we have the
00:15:38.880
ability to move that code into a different directory. So here we introduced a domain directory
00:15:44.639
with a coffee machine subdirectory and we moved the new classes there. Now this
00:15:50.560
directory structure provides a name space where we can put all of the business logic that's related to coffee
00:15:56.880
machines. It is also allowing the team to that's
00:16:02.240
in charge of the coffee machine features to take ownership and experiment with
00:16:07.920
different approaches without requiring that consensus that I I talked about before from the entire organization.
00:16:13.279
They're also now accountable for the level of test coverage or if there's you know test flakiness that's coming from
00:16:18.320
this directory. Plus, with that coffee machine namespace
00:16:24.320
in place, we can actually go and rename some of the classes that we extracted to leverage that information that's already
00:16:30.160
conveyed by the namespace.
00:16:35.920
All right, the next step we took was to teach people how to remove business
00:16:41.759
logic out um or from our views, specifically ERB files.
00:16:48.480
Now, views present a different challenge. Um, ERB files are special in that they
00:16:54.880
can't be processed by community standard uh, code quality tools like flog and
00:17:00.000
simple cuff which we use both. Um, so these view tests are also difficult
00:17:07.360
to write and they can give you a false sense of uh security because you have to
00:17:12.400
set the instance variables that the controller would normally set for you. And once you do that, now it's possible
00:17:18.880
for you to go change the controller code without breaking that view test.
00:17:24.799
Also, the embedded Ruby syntax makes it harder to read plain old Ruby. So the more logic you have in there, the harder
00:17:30.080
it is to understand what's going on. So we restructured our views to make the code easier to test and easier to
00:17:36.799
understand. And so as a bonus, we can now measure the quality of our code. We can see what the flux score is and get
00:17:43.919
coverage information. So let's look at an example.
00:17:48.960
All right. Here on the left we have a controller that's in charge of rendering configuration properties for a coffee
00:17:55.039
machine. The controller itself is not doing much. It's just finding the coffee machine and then it's setting a few
00:18:01.520
instance variable and then it renders the template. Now on the right we have the view or the ERB template and this
00:18:09.360
template contains the business logic that decides which properties get rendered under what conditions. Now the
00:18:16.160
output is just a text file with key value pairs are separated by new line characters. Just so that you have an
00:18:23.039
idea it looks like this. In this case it's just a plain text
00:18:28.240
response with a list of key value pairs. It's not even HTML. So when we looked at this response, we determined that we
00:18:34.799
don't really need ERB for this. A plain old Ruby object or a porro should be
00:18:40.320
enough. Um and again before refactoring this you
00:18:45.520
should make sure that there's at least some highle tests covering this. And assuming you have that
00:18:51.440
um let's get started. So here's the controller and the ERB template as we saw it a moment ago. Let's replace the
00:18:57.360
view with a portal. All right. So here we've created a new
00:19:02.640
class called coffee machine configuration. Now this class encapsulates the business
00:19:07.760
logic that's in charge of building those configuration properties. And we've also pulled the code that calculated the
00:19:14.640
instance variables that used to be in the controller into the new class.
00:19:20.000
Now we did that because the variables were actually deeply embedded in the views logic. The controller had no use
00:19:27.039
for those variables other than setting them so that the view can then access them and that was coupling the view in
00:19:32.080
the controller. However, we decided in this case to leave the code that finds the coffee
00:19:38.480
machine in the controller because the controller does use that logic to determine what the correct HTTP response
00:19:44.320
should be. The new class on the other hand does not even need to be instantiated if there is no coffee
00:19:50.080
machine found. Now the benefits of this particular
00:19:56.080
extraction are that the coffee machine configuration is now much easier to test than an ERB view. We can get coverage
00:20:03.600
and complexity information for the new class and the controller is also much easier to test since it's responsible
00:20:10.240
for less business logic. All right, let's look at where this code should go. So we started with coding
00:20:17.600
controllers and views and we then replace the view with a pure Ruby object.
00:20:23.919
So we can now put that uh new coffee machine configuration class in the domain coffee machine directory and
00:20:31.280
rename it to just configuration since uh coffee machine is already in the name space.
00:20:39.360
Okay, one more. The next thing we thought people to remove was the business logic
00:20:46.320
from our models. Now, this might seem unnecessary or counterintuitive to some of you given
00:20:52.880
that, you know, skinny controllers, fat models has been considered a best practice in the the Ruby Rails community
00:20:59.039
for a while. Um, and you know, they models just seem to fit seem to be the
00:21:04.880
place to put your business logic. However, there's a few reasons to move your business logic out of your models.
00:21:11.760
Most of our models represent these core concepts in our application and it makes them magnets for code. And the more code
00:21:19.200
gets added to these models, the lower the cohesion and the higher the chance for these methods that you're writing
00:21:25.360
your models to be implicitly coupled with each other. So in other words, these models just
00:21:32.000
tend to become junk roars. that diagram that Allan showed of the uh
00:21:37.440
mess of wires. We have lots of models that look like that. Um but also more
00:21:42.720
importantly, it makes it hard to write to draw boundaries around the code that's in models and to, you know, help
00:21:49.360
people take ownership of that code. So once again, we restructured our code to
00:21:55.360
make it easier to test and to draw boundaries. So let's take a look.
00:22:02.400
All right. On the left, we have a controller action that's in charge of broadcasting details about a coffee machine.
00:22:09.039
So, the controller here is going to query the database to find the correct coffee machine and then it's going to call broadcast details um on the coffee
00:22:16.720
machine model. Now, on the right, we have the coffee machine model. The broadcast details method checks various
00:22:23.440
attributes on the model and then it builds a string that contains the machine's name, recent activity, and
00:22:30.000
maybe the supply status. So this is going up so you like devices in the network can see how the coffee machine
00:22:36.480
is doing. Now before I show the extraction again I
00:22:41.919
should uh repeat that you should make sure that there is at least a highle test and assuming you have that
00:22:50.720
um let's extract the code. So here we move the logic from the broadcast
00:22:57.039
details method into a new class called coffee machine broadcast details.
00:23:02.640
The class receives a coffee machine object and performs the same logic to build a string that contains the
00:23:09.520
machine's name, recent activity, and supply status. Now this new class has a few advantages.
00:23:17.760
The first one is that it is no longer implicitly coupled to the model attributes anymore. It's not just
00:23:23.760
magically grabbing the data it needs. Um, this also makes it more extensible. As long as it receives an object that
00:23:30.320
responds to these same methods, it can be used in other contexts. For example, you could pass it an immutable data
00:23:37.200
object that provides the same information. Second, it's easier to test it since the
00:23:43.520
coffee machine object that you're passing in your initializer can be more easily stopped in your tests.
00:23:49.440
And then finally, the code in the class is no longer accessible to other methods in the model. We don't think about this
00:23:55.679
a lot, but this by pulling this logic out into a new class, you are preventing
00:24:02.080
that incidental coupling via referencing other methods or instance variables that might be set in this method.
00:24:08.880
So we have those benefits, but also one thing we wanted to be able to do is to
00:24:14.400
move it inside a boundary. We can do that now. So we started with code in um a
00:24:20.720
controller and a model. We then extracted a class out of the model and we can put that new coffee machine
00:24:27.600
broadcast details class in the domain coffee machine directory and maybe just call it broadcast details.
00:24:36.640
All right, that was a brief look at some examples of how we've extracted our code
00:24:42.400
out of these Rails concepts and started to build out these domain um spaces.
00:24:49.840
But to summarize the approach, we are putting business logic in plain old Ruby
00:24:54.960
objects and we're moving these objects into these namespaces so that we can
00:25:00.159
create boundaries around the domain concepts. Now, if we bring all of this together, all the classes we've
00:25:06.159
extracted, the directory structure is going to look like this.
00:25:11.279
Now, you may have noticed that these classes are not organized by pattern or framework concept. That's that tends to
00:25:18.320
be the first thing uh people who have a lot of experience with Rails notice like wait
00:25:23.600
um you know this update settings and update water tank settings classes, they could be considered service objects,
00:25:30.159
right? Controllers are delegating to them. And on the other hand, the broadcast details and configuration classes are
00:25:36.640
more like serializers, right? They take in some data and return to you some uh more data in a different shape.
00:25:43.679
We have explicitly decided not to be prescriptive about how the code should
00:25:49.120
be organized inside the domain directories. And we did this because it allows teams
00:25:55.039
to decide what works best for them. teams may be in different parts of their
00:26:00.559
own journey, different sizes of teams, different levels of expertise. And so
00:26:05.840
some of them organized by subdomains like they might create a water tank subdomain and move some of the code
00:26:11.120
there. And others chose to organize by pattern, for example, services and serializers.
00:26:17.039
Sometimes they actually go back and forth, whatever works for them. All right, we're just about done. I'm
00:26:24.320
going to hand it back to Alan to close us out. Okay.
00:26:30.320
All right. So there are a few ideas we want you to
00:26:35.520
from this talk. Um first remember the convex and concave payouts. Two
00:26:40.799
different investment models that can be combined into a single curve uh to model
00:26:45.840
an application's development life cycle with three phases explore, expand, and extract.
00:26:52.320
Understanding where you are on that chart can actually help you make informed decisions about the investments you're making in your application.
00:27:00.000
And understanding the route your application took to get where it is today not only builds empathy with the
00:27:05.120
developers who built it, but it also helps you think about and prepare for the application you want to have tomorrow.
00:27:12.240
Then finally, we wanted to share our approach. And due to how old our app is and due to the growth trajectory we've
00:27:17.919
been on, we found that Rails was kind of getting in our way. and also not providing enough structure.
00:27:25.039
Um, Rails and the lack of best practices in our early life led us to some super
00:27:31.440
large models and controllers with B business logic everywhere even in our views. I'm surprised I didn't get a veto
00:27:37.600
didn't get a groan when we mentioned views. Um, it's all react now.
00:27:43.120
Yeah. Uh, historically, this has kind of caused us to struggle with complexity and with testing. Now modularizing our
00:27:50.080
code allowed us to work on smaller more testable things and adding uh domain
00:27:55.919
boundaries um has allowed us to uh kind of pull apart well the modularization has
00:28:01.679
allowed us to pull apart these central concepts and then put them into areas that are bounded so that uh each team
00:28:09.200
has their own uh boundary their own domain. uh and when we've added those boundaries between domains, it's given
00:28:15.600
the teams autonomy to make improvements to their own code as well as accountability for that code.
00:28:23.679
Now, couple of things that we've seen that have turned out to be really great. Um first, we're seeing a massive
00:28:31.120
increase in test coverage. Now, the average coverage for all of our modularized code, the stuff in those
00:28:36.720
domain folders, is over 97%. uh the rest of the codebase as a whole
00:28:43.120
is about 78%. Which is close to that magic management number of 80 but like which 20% do you
00:28:50.159
not want me to test? Um so 20% is you know it's not a panacea but
00:28:57.200
it's going to it's going to be a big difference um from 97 to 78.
00:29:03.360
We're also seeing corresponding decreases in complexity. The average method complexity in our modularized
00:29:09.120
code is 30% lower than the codebase as a whole. Smaller things are less complex
00:29:15.440
and easier to test. We've also seen a big improvement. Flaky
00:29:20.799
tests and SEV one responses since it's now easy to tell which team owns the bit of code at the center of the problem.
00:29:29.200
We've also had a couple of rough spots. Uh one in particular is worth mentioning. Um, probably the hardest
00:29:34.880
thing about this has been the autonomy. Giving teams autonomy within their domains means that they might not
00:29:41.200
leverage the techniques we've described here. It's their choice. Um, and in some
00:29:46.240
cases, teams have used their autonomy in ways that have caused issues outside their domain. And the discussion around
00:29:52.640
how far that autonomy extends can be challenging. So, what's next? Well, most of the teams
00:30:00.240
uh at our organization are still in the extraction phase of this process, but there are other phases in the future.
00:30:06.000
Um, some of our teams have moved this far along. They've been able to wrap their domain in an API uh and then even
00:30:14.000
extract their service from the from the monolith. Now, we don't always encourage that necessarily, but in in the cases
00:30:20.399
I'm talking about, it made total sense. Um, and we expect that as other teams get further into the extraction portion
00:30:26.320
of the story, they may look at doing something similar.
00:30:31.520
Okay, that's about all the time we have. If you're interested to know more about our journey, uh, you can reach out to us
00:30:37.760
here or you can come find us at the booth. Uh, or to plug the silent
00:30:42.960
auction, you can talk to the modularization team. Uh, and just bid on the silent auction item. Um,
00:30:49.039
that' be me. So, uh that's it. Thank you.