Discussions about Technology within a Caribbean Context.
Introduction to Dart 3.x
Dart was intended to be a language to supplant JavaScript on the web. Dart made up for a lot of the shortcomings of JavaScript including static types and recently better null protection. Microsoft beat Google to the punch and its use of TypeScript on the web has proliferated a large portion of web-based applications.
Dart would have died if Flutter didn’t save it from its fate and chances are if you are here reading this blog post you are more interested in building Flutter applications than running Dart on the web. Don’t get me wrong, Dart has amazing features which personally I would have loved in other languages including Just-In-Time compilation to help with it’s lightning fast reloads when debugging.
During this introduction, take some time to explore and experiment with the various features of the language using DartPad. It is a user-friendly and freely accessible platform that enables you to promptly receive feedback as you test out different aspects of the language.
Data Types
The declaration of a data type is pretty much similar syntactically to most languages including JavaScript and TypeScript. The basic structure of the declaration is as follows:
type identifier = value;
In Dart the primitive/built-in data types are:
Numbers
(int, double)
Strings
(String)
Booleans
(bool)
Records
((value1, value2))
Lists (
List
, also known as arrays)Sets
(Set)
Maps
(Map)
Runes (
Runes
; often replaced by thecharacters
API)Symbols
(Symbol)
The identifier
or variable name has a few rules:
Special characters such as underscores, exclamation marks and spaces are not allowed in variable names
Must not be preceded by a number
keywords are not allowed
Type Inference
Dart, despite being a statically typed language, allows for some type inference. This can be seen in the code block below:
var value = "My Name is John";
print(value); // outputs -> My Name is John
This is perfectly acceptable and the compiler will make accomodations for the value variable
to be treated as a String.
var value = "My Name is John";
print(value);
value = 20;
If we attempt to assign a different type value to the previously assigned string the compiler will respond with the following error:
compileDDC
main.dart:5:11: Error: A value of type 'int' can't be assigned to a variable of type 'String'.
value = 20;
^
Variables
Variables store references and a fundamental to all programming languages. These references are integral to making decisions within your app.
Null Safety
Dart protects developers from themselves. The dart compiler is able to detect and stop compilation in the event that an expression may evaluate to null. Ask any JavaScript developer; one of the worst things to happen is a null reference during runtime. Dart enables null safety with 3 main changes:
Explicitly specifying that a variable is “nullable”. This means if a value has the potential to evaluate to null, then it is nullable. This is accomplished by adding a
?
character to the end of the type declaration.String? name // name can be null or string
Variables must be initialized before using them.
Calls to an object’s methods cannot be made with a nullable type.
If a variable is nullable there will be additional work expected of the developer when attempting to access the value. Examine the following function:
void main() {
int? value;
print(value + 2);
}
The compiler would emit the following error during compilation:
compileDDC
main.dart:3:15: Error: Operator '+' cannot be called on 'int?' because it is potentially null.
print(value + 2);
As good practice, it forces developers to check first if the value is null before attempting an operation with a null value. To fix this issue the following adjustments are made:
void main() {
int? value;
if(value != null){
print(value + 2);
}
}
Late variables
In many cases, a variable needs to be set later after being declared. Some of these situations include:
Initialization is expensive and takes some time/effort
Value is unavailable at the time it is declared
Dart accommodates these cases by allowing developers to use the keyword late
and tell the compiler to initialize when needed. Example:
// This is the program's only call to readThermometer().
late String temperature = readThermometer(); // Lazily initialized.
In the example, if the temperature
variable is never used, then the expensive readThermometer()
function is never called.
final
and const
For the Java Developers amongst us, these concepts should be familiar. For variables that never change values we have two options in Dart:
final
allows us to set a variable to a value once and only once.final name = “Bob”; // Without a type annotation (implicit type of String)
final String nickname = “Bobby”; // With a type
If we attempted to set the values above after the first initialization the compiler will fail.const
allows us to set a variable to a value during compilation and are therefore implicitly final.const bar = 1000000; // Unit of pressure (dynes/cm2)
const double atm = 1.01325 * bar; // Standard atmosphere
Data Structures
Most programming languages support a set of basic, but powerful constructs that allow for the temporary storage of information that is intended to be used to manipulate its contents and later present the details.
Lists
Each language presents their own implementation of some collection of ordered objects. In the case of Dart, we introduce lists. They are denoted by a set of square brackets []
:
var list = [100, 122, 334];
print(list.length); // prints 3
print(list[1]); // prints 122
list[1] = 500; // assigns the value 500 to the item in position 1
print(list[1]); // prints 500
Note:
var list = [100, 122, 334];
is inferred to be a list of integers! You won’t be able to add a string to this list.
Items in a list are ordered based on their position, starting with 0. So in the example above, the item at position 0 is 100. Items can be inserted into the list by using the add
method and removed by using the remove
method. Example:
var list = [100, 122, 334];
print(list.length); // prints 3
list.add(567);
print(list.length); // prints 4
print(list[3]); // prints 567
list.remove(100);
print(list); // prints [122, 334, 567]
Sets
A set in Dart is an unordered collection.
var categories = {'Programming', 'Life Lessons', 'Life Hacks', 'Shopping'};
Set<String> locations = {};
locations.add('Kingston');
locations.addAll({'Miami', 'Port of Spain'});
print(categories); // prints {Programming, Life Lessons, Life Hacks, Shopping}
print(locations); // prints {Kingston, Miami, Port of Spain}
print(locations.length); // prints 3
Maps
A map is a collection of key-value pairs where the key appears only once in the collection. It allows us to store associations between a key and a value, while providing convenient methods.
Map <int, String> studentRecords = {};
studentRecords[12939] = "John Doe";
studentRecords[33312] = "Mary Jane";
studentRecords[11112] = "Clark Kent";
print(studentRecords[12939]); // prints 'John Doe'
print(studentRecords.length); // prints 3
studentRecords.remove(33312);
print(studentRecords); // prints {12939: John Doe, 11112: Clark Kent}
Control Logic
In any language the ability to control the flow of operations based on your needs is essential. Dart, like most modern languages, has features that are quite standard in this regard. This includes branching and looping and they allow for the manipulation of the flow of the program within your code.
Looping
At times we need to repeat a set of operations and these language structures allow for this behaviour in a controlled way.
For Loops
There are a few ways of accomplishing a for loop. The following is the most common:
for (initilizer; condition; incrementer) {
// operations
}
The
initializer
portion of the for loop takes the variable(s) that are to be initialized to manage the loop.The
condition
is what tells the code to continue if it evaluates to true. The code will end when the condition evaluates to false.the
incrementer
allows us to change a value on each iteration of the loop
So to make sense of this, the follow example of making code count from 1 to 100 is shown:
for(int i = 0;i < 100; i++) {
print(i+1);
}
Another common use of a for loop is to perform an operation on a collection of items. The syntax is slightly different from the previous example:
for(final item in collection) {
// perform operations on item
}
In the syntax above:
item
is an individual item within a list where all items are homogenouscollection
is a Iterable type like aList
orSet
In practice the for loop will look something like this:
for (final candidate in candidates) {
candidate.interview();
}
While and Do-While Loops
A while loop iterates, but evaluates a condition before the loop:
int i = 0;
while(!bin.isFull()) {
bin.add(i++);
}
A do-while loop evaluates the condition after the loop:
do {
printLine();
} while (!atEndOfPage());
Branching
This includes if
and switch
statements that allow for some operations to happen if some criteria is met. The if
statement takes the following format:
if (condition) {
// do something
} else {
// do something else
}
The condition
section of the statement above is any logical expression that evaluates to true
or false
. These are examples of an if
statement.
if (number < 0) {
print("Number is negative");
} else if (number < 100) {
print("Number is positive but less than 100");
} else {
print("Number is positive but greater or equal to 100");
}
The if statement also has a shorter form of being written, take note of the absence of curl brackets:
if (isRaining())
closeWindows();
switch
Statements evaluates a value against a number of cases. If a case matches the value held in the variable then it will execute specific section of code:
switch(value) {
case value1:
// doSomething();
break;
case value2:
// doSomethingElse();
break;
default:
// doSomethingIfNoMatch();
break;
}
In the syntax above, you will notice the use of break
. This tells the computer to exit the switch
statement and continue executing the code outside of the switch
block. The absence of the break statement will cause the computer to continue to execute the code for subsequent cases.
Also take note of the default
case. This allows us to accomodate for values that are not accounted for, such as unexpected values or unknown cases. The code below demonstrates a use-case of a robot of some kind being told to walk:
var opCode = "WALK";
switch (opCode) {
case "KNEEL":
executeKneel();
break;
case "WALK":
executeWalk();
break;
case "SIT":
executeSit();
break;
case "SLEEP":
executeSleep();
break;
default:
executeConfused();
break;
}
break
and continue
Let’s also showcase the use of break outside of switch statements. Breaks are useful to interrupt the continued flow of code. It can be used to jump out of loops such as the example below:
while(true) { // continues indefinitely
if(shutDownRequested()) {
break;
} else {
processIncomingRequests();
}
}
This allows the computer to continue processing incoming requests until it receives some shutdown request at which point the computer will exit the loop.
Consider the case where a system must interview all candidates that have 5 or more years experience for a particular job. The system is given a list of candidates and it must interview each within the list that matches the criteria:
for (int i = 0; i < candidates.length; i++) {
var candidate = candidates[i];
if (candidate.yearsExperience < 5) {
continue;
}
candidate.interview();
}
In this case if the candidate has less than 5 years, the computer will not execute the candidate.interview();
portion of the code and will instead continue and skip to the next loop iteration.
Functions
A function can be thought of as a block of code that solves a specific task. To simplify, it helps make code easier to read and manage by keeping it neat, so developers can concentrate on specific code sections. There are several variations of functions including anonymous functions, but we will touch on a few basic ideas.
void main() {
print(isEven(2));
}
bool isEven(int number) {
return (number % 2 == 0);
}
In the example above there are two functions, main
and isEven
. You will observe that the main
function has the keyword void
preceding the function name. This tells the compiler that the function returns nothing. The isEven
function though has bool
preceding the function name. This tells the compiler that the function returns a boolean type value.
A function must have a return type defined.
The second thing to observe are the items in the parentheses. The main
function has no “parameters” and the isEven
function has one “parameter” called number
of type int
.
A function can have any number of parameters.
The third and final observation with this example is that in the isEven
function the keyword return
makes an appearance. It tells the compiler that it must return the result of evaluating the proceeding statement. In the example the statement is a comparison that checks if the number
leaves no remainder after being divided by two then it evaluates to true.
If the function has a return type other than void, then the return statement must return a value of that type.
An application will always have reference to the main()
function. It is the entrypoint to the application and kicks off all other processes within your app and always returns void
.
bool isEven(int number) => (number % 2 == 0);
The line above is actually another way of writing the same function previously described. It is a shorthand form referred to as arrow syntax that makes writing very small functions a little shorter and can be more convenient.
Named Parameters
void setText({bool? bold, bool? italics, bool? underlined}) {
// does something
}
The function setText
has a different set of parameters in comparison to the examples seen in previous examples. These are called named parameters and they are optional unless explicitly marked as required. You will notice that the type indicator has a question mark to indicate they may be null as they are optional. To call the function and specify the arguments you can do the following:
setText(bold: false, underlined: true); // bold = false, italics = null and underlined = true
To tell the compiler a default value instead of handling potential null values, you can do the following in the parameters definition:
void setText({bool bold = false, bool italics = false, bool underlined = false}) {
// does something
}
setText(bold: true); // bold = true, italics = false and underlined = false
What if you want to force your developers to add a named parameter and supply a value? We have the following option, note however that in the example, language can still be null:
const setLanguage({required String? language}){
...
}
Positional Parameters
The positional parameters can also be marked as optional. Consider the following example:
String say(String from, String msg, [String? device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
In this example, it allows us to call the function say in any of the following ways:
say("Johnny","I am Johnny Five and I am alive");
say("Mark", "Hello from the other side!", "Android Phone");
Designing Backend APIs in One Shot: Best Practices for Efficiency and Maintainability
When working on a sprint with specific objectives and the need for quick functionality delivery that can be reworked later, it's important to consider the implications of your choices. While your scrum master and project team may appreciate this approach, it can lead to unnecessary rework for both you and your team.
Instead, how about designing your APIs once and forgetting about them? Here are some guidelines that will help you stay ahead of the curve, allowing you to relax and enjoy the benefits of remote work during the next sprint.
When working on a sprint with specific objectives and the need for quick functionality delivery that can be reworked later, it's important to consider the implications of your choices. While your scrum master and project team may appreciate this approach, it can lead to unnecessary rework for both you and your team.
Instead, how about designing your APIs once and forgetting about them? Here are some guidelines that will help you stay ahead of the curve, allowing you to relax and enjoy the benefits of remote work during the next sprint.
Effective Resource Names
We often come across textbook examples of resource names like "Accounts" or "Customers," which generally work well in practice. For instance, when creating a customer, we use the endpoint POST /customers
and provide the necessary details in the request body.
Avoid the trap of building functionality with narrow focus, resulting in endpoints like POST /createCustomer
. As you delve deeper into your product backlog, you may find yourself creating unnecessary endpoints like POST /fetchCustomer
.
Stick to the CRUD (CREATE READ UPDATE DELETE) pattern:
Fetching customers ->
GET /customers
Fetching a specific customer ->
GET /customers/{id}
Creating a customer ->
POST /customers
Deleting a customer ->
DELETE /customers/{id}
Updating a customer ->
PUT /customers/{id}
Always pluralize the resource name. Notice that the resource doesn't use GET /customer
. At first glance, this might confuse a novice backend engineer into thinking that the endpoint expects a single customer instead of multiple ones. Removing assumptions in programming helps you reach the finish line faster.
Versioning Upfront!
Yes, you should consider versioning right from the start! When creating the initial version of an endpoint, it's tempting to overlook versioning. However, saving time requires adopting a maintainable approach from the beginning.
What does this mean?
So, your GET /customers
now becomes GET /v1/customers
. Introducing versioning ensures there's no confusion about which version is being used. GET /customers
is not as clear as GET /v1/customers
, and using the latter saves time during debugging.
Always prefix the version. Imagine introducing the version after the fact and including it in the URI as GET /customers/v1/
. In older endpoints, the customer identifier will likely be assumed as v1
, causing a nightmare for backwards compatibility.
Soft Deletes, NEVER Hard Delete
There are some situations where hard deletes (permanent data removal) may be allowed, but in general, it's better to provide systems that allow for easy auditability and debugging.
To remove an item after calling the DELETE /customers/{id}
endpoint, set the record status to "DELETED"
and mark the datetime when the item was removed. This approach indicates that the item has been removed and when it happened. It also helps avoid breaking dependencies.
If we remove a customer record, the account owned by that customer may no longer function as expected. Your product team may also require functionality to display removed customers, so remember to include retrieval functionality in your GET endpoints.
For example: GET /customers?includeDeleted=true
By addressing this early on, you won't need to make adjustments in future sprints, saving you time and avoiding headaches.
Pagination and Sorting
Always consider your fellow teammates, especially the frontend developers who will need pagination and sorting capabilities. By incorporating these features from the start, you reduce the burden on your servers (fetching data) and on your clients (rendering data). Most stacks offer various paging libraries, making implementation smoother.
Therefore, your customer endpoint should support some form of pagination, such as GET /customers?size=x&page=y
, and for sorting, use GET /customers?sort_by=last_name
.
Monolithic & Microservice Architectures
The appeal of a microservices architecture is undeniable. Microservices are highly scalable and resilient platforms that can maintain high throughput even if some of their partner services experience outages. The well-documented benefits of this architecture can, at times, result in extreme complexity in maintaining and extending the platform.
The appeal of a microservices architecture is undeniable. Microservices are highly scalable and resilient platforms that can maintain high throughput even if some of their partner services experience outages. The well-documented benefits of this architecture can, at times, result in extreme complexity in maintaining and extending the platform.
Moving away from the monolithic architectures of the past is a natural trend, and while the benefits are significant, building out these systems requires a considerable amount of work. These systems need to be designed with careful attention to maintaining the separation of concerns between each microservice and ensuring that the service is loosely coupled from the overall solution.
I will try my best to decompose some of the challenges I have personally faced with the build out of a finance-based system.
Operational Overhead and Cost
A microservices architecture is highly distributed and independent, which requires the development or purchase of specialized tools to manage and support the infrastructure. However, having an entire DevOps team equipped with these tools can become expensive. In comparison, monolithic platforms are more easily managed and maintained with a smaller team.
Data Consistency
In a microservices architecture, each microservice has its own data source that is not shared with other services. If a data source is coupled to multiple microservices, a shared dependency is created, which can lead to issues with all microservices using the service in the event of a failure.
However, when multiple microservices use the same or similar information, data consistency can become a potential issue. Attempting to create and maintain consistent data across multiple services can be challenging and time-consuming, and inconsistent data can lead to serious issues. While there are several strategies to avoid data inconsistency, the implementation considerations can be complex and require careful attention.
Testing Challenges
In a monolithic architecture, testing is relatively simple, and it involves ensuring high unit test coverage for each module and testing the entire system to ensure end-to-end flows remain functional after each modification. However, with a microservice platform, testing becomes more complex. It requires unit testing for each microservice, testing microservices as a whole, testing end-to-end flows by coordinating calls via automated testing platforms, performing API contract testing, and using service virtualization.
This complexity is due to the distributed nature of the system, and the need to test each microservice independently as well as collectively. In addition to unit testing and testing the system as a whole, testing requirements for microservices architecture can also include service discovery and registration, versioning, routing, and load balancing. Furthermore, API contract testing and service virtualization can be necessary to ensure consistency and compatibility between microservices.
Public vs Private Healthcare
Healthcare in the public sector is beset with challenges ranging from limited resources, aging equipment and a revolving door of unmotivated staff and an overall demotivated team. This article is written based on my own experiences in two Caribbean countries. It showcases some key differences between the behaviour of staff in public and private hospitals in the region.
Healthcare in the public sector is beset with challenges ranging from limited resources, aging equipment, a revolving door of unmotivated staff, and an overall demotivated team. This article is written based on my own experiences in two Caribbean countries. It showcases some key differences between the behaviour of staff in public and private hospitals in the region.
Efficient Delivery
Private hospitals exist for profit and are always searching for a method of competing and reducing operating costs all while increasing profits. Much of the efficiencies sought after come with a continuous analysis of processes. This analysis may be purely derived from the need to ensure the highest levels of certifications are achieved or maintained which attracts patients to their facility.
From my observations in the public sector the need to improve the efficiency is one that is a lot more reactionary. Did someone experience a fall down these stairs? Oh no - let's ensure that patients in our care are not at risk of injury as a result of aging infrastructure. The need to improve efficiencies are never driven by a strive to compete in the sector. Public hospitals exists to only service the needs of the community and usually only worry about reducing costs in the face of budget cuts.
The contrasts in my experience are night and day - any physician operating in both worlds can support this view. A private hospital is a proactive institution while a public hospital is reactionary.
Growth requires Reinvestments
The need to increase efficiencies usually come at some costs. During the analysis phase it might be noted that the introduction of a Pyxis Pharmaceutical Dispenser may save the hospital thousands of dollars over a 5 year period and reduce the need for one on staff pharmacist. For a private hospital this is an easy sell - The Pyxis will be paying for itself easily within a few years of being introduced despite the initial high upfront cost. Private Hospitals are also not shackled to bureaucratic red tape to procure a high valued item; instead, where there is a justified spend - this can easily be approved by a board that values the need for increased operational efficiencies.
In a publicly funded hospital there is a stark difference in the approach:
Who will pay for it? Usually, public hospitals in the Caribbean are poorly funded and rely heavily on private donors to invest in infrastructure - much of what is critical to the care of patients.
Reductions in staffing. If the efficiency increases due to the introduction of a new process- in this case, a machine - then it is not likely to see a change in the staff complement. This is usually despite the fact that the new process can now be done with less. In the majority of cases, it is true that the staffing was never adequate to begin with.
Laser Focused Team
A team in the public sector, based on my observation, does not need to ever focus on identifying a process that can be improved. They must operate in an environment that protects themselves and delivers healthcare with just the resources at hand, leaving a focus on the profit to the finance team upstairs. This mindset from staff results in issues with billing, unrealized costs, abuses in resources and further degradation of the quality of patient care due to unnecessarily high costs. There are a number of factors that lead to this behaviour; the bureaucracy involved in processes, the lack of incentives and a lack of involvement in efficiency management that is needed from all members of the team.
Summary
The delivery of healthcare in the Caribbean between private and public hospitals is wide and varied. The outcomes are unfortunate but can be improved. In a future article, I will look at a few suggestions to tackle the disparity and close the gap in the quality of healthcare facilities.
Talent Retention
Brain Drain happens when a country fails to secure its most highly educated citizens leaving a vacuum of competency in several sectors. This is felt heavily in the ICT sector with a higher dependence on outsourcing outfits playing an increased role in staffing Caribbean organizations. Brain drain has a wide reaching impact on economies with more monies leaving the region and building other, more developed companies, that can retain and attract the talent required. It is proposed that a slight change of approach to talent retention may be beneficial to Jamaica and the wider Caribbean region.
Brain Drain happens when a country fails to secure its most highly educated citizens, leaving a vacuum of competency in several sectors. The phenomenon has wide reaching impacts on economies, especially in developing countries where talent now has to be outsourced to companies from more developed countries that are better equipped to retain talent. For example, in Jamaica the ICT sector contributes heavily to brain drain in a somewhat self fulfilling prophecy. ICT companies will outsource to foreign companies instead of using their resources to hire local talent and in turn, because there is limited focus on hiring locally, many citizens are compelled to leave. In some instances they may even work at the foreign entities these Caribbean firms outsource. There must be a change in the approach of to talent acquisition and retention that would be beneficial to Jamaica and the wider Caribbean.
It is known that Jamaica ranks highly on the Human Flight and Brain Drain Index, second only to Samoa. If we ever needed a confirmation of what has been seen from many families in Jamaica - then this ranking is it. Interestingly our Caribbean neighbors, the Bahamas, ranked 120th on the index.
I have participated in several recruitment activities in Jamaica, focused on identifying talent from an early stage and worked to engage and secure the young and brilliant minds before being snatched up by the likes of Google, Meta and Amazon. Recruiting isn’t easy as I have had to formulate convincing arguments to attract talent and persuade them to remain in Jamaica. Considering the realities of high inflation and interest rates which has only been exacerbated by global conflicts such as in Europe where there is rapid rise in food and fuel costs, it has proven difficult. The outlook seems bleak and increasingly disparaging to a company looking to participate in a global economy.
The grass is always greener..
A young graduate in 2022 has more access to large multinational firms than they did in 2009. The COVID-19 pandemic proved to be a catalyst in the era of remote work and since then the world has seen a sharp increase in remote work opportunities with better incentives to foster a healthier balance between work and personal life. High-paying remote work opportunities also increased from 4% (December 19, 2019) to approximately 15% in 2022. I am inclined to believe that remote work is here to stay. With increased accessibility, better working terms and a higher compensation package, traditional company structures and practices will fail to attract the kind of talent they would need to succeed. My prediction is that individuals will become uninterested in recruitment drives and even more disenchanted with compensation packages that promise health insurance at most.
Jamaican companies have now seen its own mass resignations especially during the pandemic where productivity remained high - but demands increased. Managers are now asked to participate in employee engagement check-ins and one-and-ones to ensure that staff remain motivated and committed to the company. The effectiveness of these check-ins are constantly eroded by migration push and pull factors. Jamaican companies rely too heavily on foreign firms which instead of encouraging their employees to stay it encourages them to leave. It is difficult surmise their value within the company when hired foreign consultants earn more but are required to do the same job or less.
Cauterize the wound
It takes a sudden dramatic act to stop the loss of tech talent. It involves acknowledging the hard truths that employees will work if given the right tools, environment and a healthy work-life balance that fosters personal development.
Pay talent - retain talent:
It goes without saying that the demand for talent must be met with a salary that can retain said talent. As a former part-time lecturer in the Department of Computing at the University of the West Indies, I have seen companies such as Goldman Sachs or Google appreciate our own local talent and lure them with attractive and unmatched compensation before they've even received a degree in hand. Why don't we appreciate our own similarly?
Dispel the myth that North American knows best:
Too often do we look outside to bolster our teams with talent that do not understand the Jamaican context. I have been involved in discussions with consultants from Fortune 100 companies that have sought to influence product direction with their own misconceived notions of demand for features. I have been fortunate to see several instances where, under the guidance of local talent, these product directions are stymied with the appropriate research and proven with adoption with locally developed ideas. We know our audience better than anyone else does.
Remote Work Destination:
Jamaica is an amazing country that enjoys wonderful and consistent weather year-round. People dream of sitting on the beach and writing code - yet it's not taken advantage of. During the pandemic we saw a myriad of countries opening up borders to facilitate remote work and increase potential revenues from these workers spending.
Summary
The brain-drain is bad, but it can be slowed and maybe even stopped. With a simple reallocation of time and resources it is very possible. The continued over dependence on larger firms to bolster the Jamaican workforce, is simply not sustainable.