DEV Community

Alexander Shagov
Alexander Shagov

Posted on

The "IO to the Boundary" principle

In software architecture, many principles and patterns have emerged over time. However, a lot of these can be boiled down to a single idea: the "IO to the Boundary" principle. This principle suggests that all input/output operations, like database queries, API calls, or file system interactions, should be pushed to the edges (or boundaries) of your application.

Let's look at what some other ideas tell us:

  • Functional Core, Imperative Shell: This principle suggests keeping the core of your application pure and functional, while handling side effects (IO) in an outer layer. This is basically another way of saying "IO to the Boundary."
  • Clean Architecture: Proposed by Robert C. Martin, this architecture emphasizes separating concerns and having dependencies point inwards, which fits with keeping IO at the edges.
  • Command Query Responsibility Segregation (CQRS): While not mainly about IO boundaries, CQRS often leads to separating read and write operations. This separation can support the "IO to the Boundary" principle by making it easier to isolate and manage IO operations at the system's edges.

Example

  1. The Client Code interacts with both WeatherAPI (to create an object instance) and WeatherReport (to generate a report).
  2. WeatherReport uses WeatherAPI internally to fetch temperature data.
  3. WeatherAPI encapsulates the IO operations, making the actual HTTP request to the External Weather API.

This structure keeps the core application logic (WeatherReport) free from direct IO concerns, pushing those responsibilities to the edges (WeatherAPI and beyond).

Client Code WeatherReport WeatherAPI External Weather API | | | | | | | | | new WeatherAPI() | | |---------------------------------------> | | | | | | new WeatherReport(weather_api) | | |------------------>| | | | | | | | generate_report('New York') | | |------------------>| | | | | | | | | get_temperature('New York') | | |------------------>| | | | | | | | | HTTP GET request | | | |----------------->| | | | | | | | JSON response | | | |<-----------------| | | | | | | temperature | | | |<------------------| | | | | | | formatted report | | | |<------------------| | | | | | | [Application Boundary] [IO Boundary] 
Enter fullscreen mode Exit fullscreen mode

Note on oversimplification

From my perspective, the key to effective architecture is finding the right balance between simplification and necessary complexity (sounds vague but that's true..). It's becoming clear that most mid-sized projects don't need the level of complexity they often include. But some do. While I recognize that the real world is more complex, with evolving projects, changing teams, and loss of knowledge when an organization changes, it's crucial to remember that sometimes the best abstraction is no abstraction at all.

Code samples

Let's consider "good" and "bad" examples (I'll be using Ruby and Ruby on Rails to convey the ideas)

Bad example

# app/controllers/weather_controller.rb class WeatherController < ApplicationController def report city = params[:city] api_key = ENV['WEATHER_API_KEY'] response = HTTP.get("https://api.weather.com/v1/temperature?city=#{city}&key=#{api_key}") data = JSON.parse(response.body) temperature = data['temperature'] feels_like = data['feels_like'] report = "Weather Report for #{city}:\n" report += "Temperature: #{temperature}°C\n" report += "Feels like: #{feels_like}°C\n" if temperature > 30 report += "It's hot outside! Stay hydrated." elsif temperature < 10 report += "It's cold! Bundle up." else report += "The weather is mild. Enjoy your day!" end render plain: report end end 
Enter fullscreen mode Exit fullscreen mode

It's evident that the code smells:

  • Mixing IO operations (API call) with business logic in the controller.
  • Directly parsing and manipulating data in the controller.
  • Generating the report text within the controller action.

Good example

# app/controllers/weather_controller.rb class WeatherController < ApplicationController def report result = WeatherReport::Generate.new.call(city: params[:city]) if result.success? render plain: result.value! else render plain: "Error: #{result.failure}", status: :unprocessable_entity end end end # app/business_processess/weather_report/generate.rb require 'dry/transaction' module WeatherReport class Generate include Dry::Transaction step :validate_input step :fetch_weather_data step :generate_report private def validate_input(city:) schema = Dry::Schema.Params do required(:city).filled(:string) end result = schema.call(city: city) result.success? ? Success(city: result[:city]) : Failure(result.errors.to_h) end def fetch_weather_data(city:) result = WeatherGateway.new.fetch(city) result.success? ? Success(city: city, weather: result.value!) : Failure(result.failure) end def generate_report(city:, weather:) report = WeatherReport.new(city, weather).compose Success(report) end end end # app/business_processess/weather_report/weather_gateway.rb class WeatherGateway include Dry::Monads[:result] def fetch(city) response = HTTP.get("https://api.weather.com/v1/temperature?city=#{city}&key=#{ENV['WEATHER_API_KEY']}") data = JSON.parse(response.body) Success(temperature: data['temperature'], feels_like: data['feels_like']) rescue StandardError => e Failure("Failed to fetch weather data: #{e.message}") end end # app/business_processess/weather_report/weather_report.rb class WeatherReport def initialize(city, weather) @city = city @weather = weather end def compose # ... end end 
Enter fullscreen mode Exit fullscreen mode

What has changed:

  • Separating concerns: The controller only handles HTTP-related tasks.
  • Using dry-transaction to create a clear flow of operations, with each step having a single responsibility.
  • Pushing IO operations (API calls) to the boundary in the WeatherAPI service.

Note on complexity

We should be realistic though, if we're talking about a simple 1-pager app generating the weather reports, well, who cares? It just works and we definitely do not want to introduce any additional layers.
However, if we care about the future extendability, thinking about these kind of things is crucial.

Final note

Many of the architectural principles and patterns we encounter today are not entirely new concepts, but rather evolved or repackaged ideas from the past. I hope that you see now that the "IO to the Boundary" principle is one such idea that has been expressed in various forms over the years.

Comments / suggestions appreciated! 🙏

Top comments (0)