00:00:16.480
Okay, so thanks for coming. Today I'm talking about Rails as an SOA client.
00:00:21.520
In the beginning, there was a Rails application. Most of these Rails applications started off as straightforward and simple. They typically communicated with a single database, and this is how many Rails applications begin. They're green fields, and we often connect to that single database.
00:00:35.680
However, quite soon, particularly nowadays, we start communicating with services. These services may be external, like Twilio or Twitter, or they could be internal services used as part of our enterprise operations. As time has progressed, I've observed that our Rails applications are becoming increasingly dependent on these services. There are two primary forces at play here.
00:01:05.760
The first force is that as Rails matures and our community grows, Rails applications have evolved into large monoliths—these bloated applications. Now, there's a broad movement to break up our significant monolithic Rails applications into microservices. Naturally, something needs to interface with these services, and most often, that responsibility falls on a Rails application acting as the front end to those services.
00:01:39.840
The second significant force comes from the fact that Rails applications, which initially gained prominence with startups, have increasingly found their way into enterprise environments filled with numerous services. In these enterprise scenarios, Rails applications often need to communicate with multiple services to perform their tasks. In extreme cases, we have Rails applications that operate entirely without local data storage, relying solely on these external services to accomplish their work.
00:02:11.840
I recently built an application that exemplifies this concept, and I'd like to share the techniques we used to successfully create a services-only application. My name is Pete, and I work for a consulting company called ThoughtWorks. We collaborate with clients to help them build software and improve their software development practices.
00:02:33.200
I've been with ThoughtWorks for about four years, and as you can tell from my accent, I'm based in the San Francisco office. Throughout my time at ThoughtWorks, I've engaged in a variety of projects—ranging from Ruby and Rails to Scala, JavaScript, and iOS development—and I've worked in diverse organizational contexts, from small startups to massive banks.
00:03:06.400
One of the enjoyable aspects of working at ThoughtWorks is the opportunity to operate in many different environments. We often take valuable ideas from one community and apply them in a different context. For instance, we might take concepts from the Rails community and bring them to Scala or introduce mobile application ideas into client-side JavaScript applications. It's fascinating to exchange ideas that have worked well in startups and apply them within the enterprise, and vice versa.
00:03:49.600
Today's presentation will have two parts. In the first part, I will discuss how to navigate this ecosystem and share some tools and techniques that can help in this context, particularly regarding Rails. Then, I will delve into the nitty-gritty details of how to build these services in our Rails applications.
00:04:19.040
First, let's consider the balance between co-dependence and independence. In a recent project I worked on, we developed a small Rails application that served as the front end for a large array of services. This online store was launched by a major book retailer looking to enter a new market and shift away from Java to a greenfield Rails app while leveraging existing services built in various languages.
00:04:56.400
The homepage of the application featured a list of products, prices, descriptions, and deals of the day. Interestingly, the information powering this page came from numerous sources, and our Rails app was primarily responsible for stitching that information together. We accessed the product service for catalog data, while the deals service informed us of daily promotions. Essentially, the Rails app acted as an orchestrator, retrieving data from various sources and combining it for display.
00:05:31.600
In the case of this application, you encounter an intriguing phenomenon: the app itself cannot function without these dependent services. If those services were unavailable, the app would be rendered useless. We refer to this as co-dependence. This situation leads us to an ideal goal—independence—so we wish to be able to run, test, and debug our application in isolation without needing the entire enterprise set up on our local systems.
00:05:57.920
The need for independence becomes even more pronounced when considering team dynamics. In our scenario, we were the 'red team' building the Rails application, while other teams maintained the services we depended on. This could lead to complications, as sometimes we had very little communication with the other teams and struggled with unclear lines of contact—this creates interesting side effects.
00:06:35.680
An observation is that the structure of teams often mirrors the structure of the services they maintain, leading us to Conway's Law. Conway's Law, coined in the 1960s, asserts that the communication patterns of a software system typically reflect the communication patterns of the people who created that system. This insight highlights the crucial role of team organization in software architecture and design.
00:07:04.560
It's essential for us as software engineers to understand this relationship. For over fifty years, empirical evidence has reinforced this concept. Therefore, while you cannot eliminate Conway's Law, you can choose to leverage it to your advantage rather than allowing it to work against you.
00:07:34.880
In our situation, we had three teams—let's call them the red, green, and blue teams—each responsible for their respective services. When a bug occurred, as in the example of missing prices in the deals section, we entered a game I like to call 'Who's Bug Is It Anyway?' which highlights the tension between the needs for independence and codependence.
00:08:06.960
When we encountered a problem such as this, we needed to determine whether the issue lay with the deals service, the product service, or possibly within our own Rails application's rendering. The fundamental challenge is that we are codependent while wishing for independence. Some teams choose to fully embrace co-dependence, setting up local copies of the services for testing. This is more feasible today thanks to tools like Vagrant and Docker, but successfully replicating an entire enterprise is unrealistic.
00:08:48.000
Another common approach is to rely on shared services, utilizing a common development or staging environment. However, we took a different route for our team—we aimed to foster independence as much as possible. To achieve this, we replaced our dependence on services with fake services. This meant that when testing our application, we could do so independently.
00:09:22.800
By creating mock versions of the services we depended on, we could run what I refer to as 'bounded integration tests.' These tests assess the entire Rails application's stack—from the HTML through to the network, but crucially, they do so in isolation. Our tests were designed to run without actually hitting the shared services, and interestingly, this test boundary often aligned closely with our team structure.
00:09:57.600
Using Conway's Law to our advantage helped us maintain focus on building quality software. Now, if we wanted to test for potential issues such as Unicode handling, we could establish a fake product service and simulate various responses. This ability to run tests in isolation provided us with a level of confidence that wasn't available if we were reliant on the actual services.
00:10:28.960
For each service dependency, we could create a fake version of that service. With our bounded integration tests in place, we gained a great deal of assurance that any potential bugs were not originating from our team's code, allowing us to maintain focus on identifying issues and assisting our colleagues working on other teams.
00:10:56.480
Further, we employed contract tests, often referred to as consumer-driven contracts. These involve writing test code delineating our expectations for how a dependent service should behave, which we then run against the actual version of that service. This unique approach allows us to detect issues not just within our own code, but also in the services maintained by other teams.
00:11:24.480
As we executed contract tests, we established a contract between our team and the team maintaining the external service. Often, we encounter scenarios where the API documentation is outdated. Addressing this concern, our contract tests allow us to express our expectations in coded form.
00:11:50.080
On our team, we wrote contract tests for each of our dependencies. This approach ultimately proved to be the most productive endeavor for us—significantly improving the efficiency of our software development process. It also enhanced communication between our team and other teams.
00:12:18.080
To create contract tests, we utilized standard RSpec, enabling us to make network calls to our dependencies and validate that the responses matched our expectations. Additionally, there are more advanced tools available like Pact and Pacto, which can enhance your experience if you seek a robust solution.
00:12:52.080
After implementing our bounded integration tests and contract tests, we ended up with a comprehensive CI dashboard. Each time we checked in code, we would run unit tests, functional tests, and if those passed, we were able to execute our bounded integration tests. Following that, we'd run end-to-end tests to verify as much of the application stack as possible.
00:13:25.600
In an ideal scenario, everything would pass; however, in reality, our tests often revealed failures in the end-to-end tests. This situation could lead to us being in a constant state of firefighting—trying to understand why things weren’t functioning correctly in production or staging. Thankfully, the system of contract tests enabled us to isolate precisely which service was at fault.
00:14:05.440
For instance, if service D's contract tests were failing, we could focus our attention there to investigate the issue. We would typically review logs from our CI system, looking at the request we sent and the response we received. Often, it became apparent that a bug lay in the service; for example, they might have forgotten to activate the database after completing a deployment.
00:14:39.680
This process of identifying and communicating issues allowed us to streamline our workflows. While our work was not without challenges, having those contract tests significantly facilitated smoother interactions with other teams, minimizing time spent on issues.
00:15:05.760
Now let's switch gears to discuss building these services within our team. I want to introduce the concept of service gateways, specifically focusing on a gem called Faraday. To put things into context, we must first understand another gem called Rack.
00:15:48.240
Rack is an excellent abstraction that sits between HTTP servers, providing a clean interface for handling requests and responses. By utilizing this abstraction, we can insert various middleware components between our application and the underlying HTTP server. As a request comes in from the world, it flows through the middleware stack, which can modify the request or have side effects before the Rails app processes it.
00:16:16.960
The same principle applies to Faraday—it serves as an HTTP client abstraction, allowing us to set up a middleware stack for outgoing requests. The process of sending out requests also passes through this middleware, where modifications may occur, just as it does for incoming requests.
00:16:51.200
In this example, we create a service gateway class that facilitates connections to external services using Faraday. Over time, we can build out a Faraday middleware stack configured to handle various tasks such as JSON processing, logging, and redirect following—all invisible to the core logic of our application.
00:17:14.560
The value in using Faraday lies in its ability to abstract away complex configurations, allowing our core application logic to focus solely on the business domain. This segregation between technical details and business concerns means our Rails application talks about 'books,' 'products,' and 'prices,' rather than dealing with the nitty-gritty aspects of HTTP.
00:17:42.320
With this segregation approach, we focused on separating out technical concerns from the core of our application. We implemented service gateways to manage operations like serialization and caching, keeping our primary application focused on its domain.
00:18:10.720
Our handling of serialization means that rather than processing raw JSON or XML data, we convert our stream of bytes or strings into useful domain objects as quickly as possible. We’d map the generic data structure returned from an API into meaningful objects our application could work with without exposing underlying technical complexities.
00:18:35.680
Additionally, as responses from services tend to be large, often we only require a small subset of the information returned. Therefore, we carefully extract just the data points that are valuable to our application's domains—such as the title, author, and so forth—while ignoring irrelevant details.
00:19:06.560
For the mapping functionality, we abstracted the typical boilerplate code into a cleaner, declarative approach. To achieve this, we developed a library called LazyDoc, which helps to efficiently map response data into usable domain objects.
00:19:38.560
Moreover, we implemented a caching mechanism, particularly crucial in an environment where product prices can fluctuate rapidly. While pricing information changes frequently, product data tends to be relatively static. By creating smart caching strategies, we ensured that we could optimize performance without constantly hitting our services.
00:20:14.880
Instead of the typical implementation where we would set up a localized cache repository to manage our cached product information, we opted to rely on caching headers provided by the APIs themselves. This way, we implemented caching at the service level—allowing us to benefit from built-in efficiencies rather than reinventing the wheel.
00:20:47.440
By aligning our caching mechanisms with industry standards, we maximized existing capabilities and maintained a level of flexibility. Consequently, when teams managing services recognized the need to adjust caching rules, they could do so without necessitating any code changes or redeployments on our side.
00:21:24.000
In conclusion, our principles emphasize the significance of embracing the web and its capabilities while applying proven architectural patterns in our systems. Conway's Law should guide how we structure our teams and build our software—it truly reflects the interconnectedness of communication within software development.
00:21:48.800
I encourage you all to explore concepts that deepen your understanding of software architecture and team dynamics. Embracing techniques such as domain-driven design and hexagonal architectures will undoubtedly support your journey in developing effective software solutions.
00:22:15.680
Remember, we should aim to build systems that are inherently part of the web, leveraging its rich ecosystem and established principles. This is the path to creating resilient, efficient applications that can thrive in changing environments.
00:22:43.360
Thank you for your attention. I hope you found this presentation insightful, and I encourage you to take these principles and apply them in your work to foster better systems and collaborative environments.
00:37:03.440
Thank you.