A common mistake when making the transition to microservices is to ignore the monolith and just build new features as services. This usually happens when a team feels that the monolith has gotten so out of control, and the code so unwieldy, that it would be better to declare bankruptcy and leave it to rot. This can be especially tempting because the idea of building green field code with no legacy baggage sounds a lot nicer than refactoring brittle, legacy code.
Resist the temptation to abandon your monolith. To successfully decompose your monolith by business capability and start evolving it into a set of nicely factored, single-responsibility microservices, you'll need to make sure that your monolith code base is in good shape and is well factored, and well tested. Otherwise, you'll end up with a proliferation of new services that don't model your domain cleanly (because they overlap with functionality in the monolith), and you'll continue to have trouble working with any code that exists in your monolith. Your users won't be happy and your teams' energy will most likely start to decline as the weight of technical debt becomes unbearable.
Instead, take constant, proactive steps to refactor your monolith using good, solid design principles. Excellent books have been written on the subject of refactoring (I recommend Refactoring by Martin Fowler and Working Effectively with Legacy Code by Michael Feathers), but the most important thing to know is that refactoring is never an all-or-nothing effort. Few product teams or companies will have the patience or luxury to wait while an engineering team stops the world and spends time making their code easier to change, and an engineering team that tries this will rarely be successful. Refactoring has to be a constant, steady process.
However your team schedules its work, make sure you're reserving an appropriate time for refactoring. A guiding principle is, whenever you go to make a change, first make the change easy to make, then make the change. Your goal is to make your monolith code easier to work with, easier to understand, and less brittle. You should also be able to develop a robust test suite that will come in handy.
Once your monolith is in better shape, you can start to continuously shrink the monolith as you factor out services. Another aspect of most monolith code bases is serving dynamically generated views and static assets served through browsers. If your monolith is responsible for this, consider moving your web application component into a separately served JavaScript application. This will allow you to shrink your monolith from multiple directions.
Refactoring any code base is a process. For monoliths, there are a few techniques that can work quite well. In this example, we'll document the steps that can be taken to make refactoring a Ruby on Rails code base easy:
- Using the techniques described in previous recipes, identify business capabilities and bounded contexts within your application. Let's focus on the ability to upload pictures and videos.
- Create a directory called
app/services
alongsidecontrollers
,models
, andviews
. This directory will hold all of your service objects. Service objects are a pattern used in many Rails applications to factor out a conceptual service into a ruby object that does not inherit any Ruby on Rails functionality. This will make it easier to move the functionality encapsulated within a service object into a separate microservice. There is no one way to structure your service objects. I prefer to have each object represent a service, and move operations I want that service to be responsible for to that service object as methods. - Create a new file called
attachments_service.rb
underapp/services
and give it the following definition:
class AttachmentsService def upload # ... end def delete! # ... end end
- Looking at the source code for the
AttachmentsController#create
method in theapp/controllers/attachments_controller.rb
file, it currently handles the responsibility for creating theAttachment
instance and uploading the file data to the attachment store, which in this case is an Amazon S3 bucket. This is the functionality that we need to move to the newly created service object:
# POST /messages/:message_id/attachments def create message = Message.find_by!(params[:message_id], user_id: current_user.id) file = StorageBucket.files.create( key: params[:file][:name], body: StringIO.new(Base64.decode64(params[:file][:data]), 'rb'), public: true ) attachment = Attachment.new(attachment_params.merge!(message: message)) attachment.url = file.public_url attachment.file_name = params[:file][:name]
attachment.save json_response({ url: attachment.url }, :created) end
- Open the newly created service object in the
app/services/attachments_service.rb
file and move the responsibility for uploading the file to theAttachmentsService#upload
method:
class AttachmentsService def upload(message_id, user_id, file_name, data, media_type) message = Message.find_by!(message_id, user_id: user_id) file = StorageBucket.files.create( key: file_name, body: StringIO.new(Base64.decode64(data), 'rb'), public: true ) Attachment.create( media_type: media_type, file_name: file_name, url: file.public_url, message: message ) end def delete! end end
# POST /messages/:message_id/attachments def create service = AttachmentService.new attachment = service.upload(params[:message_id], current_user.id, params[:file][:name], params[:file][:data], params[:media_type]) json_response({ url: attachment.url }, :created) end
- Repeat this process for code in the
AttachmentsController#destroy
method, moving the responsibility to the new service object. When you're finished, no code inAttachmentsController
should be interacting with theAttachments
model directly; instead, it should be going through theAttachmentsService
service object.
You've now isolated responsibility for the management of attachments to a single service class. This class should encapsulate all of the business logic that will eventually be moved to a new attachment service.