Matic
Published in

Matic

Illustration by Darya Kozemchuk

Communication layer design for Ruby microservices

  • How to organize communication between main application and service(s) in the way you don’t need to make lots of changes (code duplications) in different code bases.
  • How to test all the parts together to be sure you don’t have “interfaces mismatch”.

“Book Reader” application

Illustration by Darya Kozemchuk

Book Storage Client

Illustration by Darya Kozemchuk
  • HTTP client initialization (with authentication logic)
  • Classes for initiating API calls to and from “Book Storage”
  • Serialization/deserialization logic
  • Mapping to “Book Storage” models
  • it creates a signature for each request and adds a few headers needed for authorizing request. Note, that Authenticator class is also a part of the gem and is used both in application and service.
  • it transforms RestClient exceptions into domain specific ones.
module BookStorageClient
class BookFetcher < BaseFetcher
def all
deserialize_array(client.get(“/api/books”))
end

def find(uuid)
deserialize(client.get(“/api/books/#{uuid}”))
rescue BookStorageClient::Errors::ResourceNotFound
nil
end

private
def serializer
BookStorageClient::BookSerializer
end
end
end
module BookStorageClient
class BaseFetcher
delegate :deserialize, :deserialize_array, to: :serializer
private
def serializer
raise ‘should be implemented’
end
def client
Client.new(BookStorageClient::Config)
end
end
end
module BookStorageClient
class BaseSerializer < ActiveModel::Serializer
class << self
def serialize(object)
serializer = new(object)
ActiveModelSerializers::Adapter.create(serializer).to_json
end

def serialize_array(objects)
serializer = ActiveModel::Serializer::CollectionSerializer.new(objects, serializer: self)
ActiveModelSerializers::Adapter.create(serializer).to_json
end
def deserialize(data)
deserialize_one(JSON.parse(data))
end
def deserialize_array(data)
array = JSON.parse(data)[‘data’]
array.map { |hash| deserialize_one(‘data’ => hash) }
end
private
def deserialize_one(data)
hash = ActiveModelSerializers::Deserialization.jsonapi_parse!(data)
attrs = _attributes.map { |attr| hash[attr] }
deserializable_model.new(*attrs)
end
end
end
end
module BookStorageClient
class BookSerializer < BaseSerializer
attributes :id, :uuid, :title, :author

def self.deserializable_model
::BookStorageClient::Book
end
end
end
def index
render json: BookStorageClient::BookSerializer.serialize_array(@books)
end
def show
render json: BookStorageClient::BookSerializer.serialize(@book)
end
module BookStorageClient
Book = Struct.new(*BookStorageClient::BookSerializer._attributes)
end
module BookStorageClient
class BookHook < BaseHook
PATH = ‘callbacks/book_storage/books’.freeze

def updated(book)
client.post(PATH, book: serialize(book))
end
protected
def serializer
BookStorageClient::Hooks::BookSerializer
end
end
end
module BookStorageClient
module Hooks
class BookSerializer < BaseSerializer
attributes :id, :uuid

def self.deserializable_model
::BookStorageClient::Hooks::Book
end
end
end
end

Testing

RSpec.configure do |config|
config.before(:all, type: :book_storage_client) do
@puma_thread = Puma::Server.new(Rails.application).tap do |s|
s.add_tcp_listener(‘localhost’, 3456)
end.run
end
config.after(:all, type: :book_storage_client) do
@puma_thread.kill
end
end
describe BookStorageClient::BookFetcher, type: :book_storage_client do  let(:fetcher) { described_class.new }

describe "fetcher.find" do
before { create(:book, uuid: ‘uuid’) }
let(:book) { fetcher.find(‘uuid’) }
it "return BookStorageClient::Book object" do
expect(book).to be_a?(BookStorageClient::Book)
end
# — — —
end
end

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store